Merge pull request #4759 from FinnStutzenstein/optionalAgendaItem

Optional agenda items
This commit is contained in:
Emanuel Schütze 2019-06-24 12:37:29 +02:00 committed by GitHub
commit 3d573441ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 549 additions and 210 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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