diff --git a/client/src/app/core/repositories/agenda/item-repository.service.ts b/client/src/app/core/repositories/agenda/item-repository.service.ts index c0a6b0cff..818869af6 100644 --- a/client/src/app/core/repositories/agenda/item-repository.service.ts +++ b/client/src/app/core/repositories/agenda/item-repository.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@angular/core'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; 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 { BaseViewModelWithAgendaItem, - isBaseViewModelWithAgendaItem + isBaseViewModelWithAgendaItem, + IBaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; 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 { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository'; import { BaseHasContentObjectRepository } from '../base-has-content-object-repository'; +import { Identifiable } from 'app/shared/models/base/identifiable'; /** * Repository service for items @@ -105,16 +105,10 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository< agendaItem.content_object.collection, agendaItem.content_object.id ); - if (!contentObject) { + if (!contentObject || !isBaseViewModelWithAgendaItem(contentObject)) { return null; } - if (isBaseViewModelWithAgendaItem(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.` - ); - } + return contentObject; } /** @@ -124,19 +118,6 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository< 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 { - 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 * 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); } - /** - * Get agenda visibility from the config - * - * @return An observable to the default agenda visibility - */ - public getDefaultAgendaVisibility(): Observable { - return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key)); + public async addItemToAgenda(contentObject: IBaseViewModelWithAgendaItem): Promise { + return await this.httpService.post('/rest/agenda/item/', { + collection: contentObject.collectionString, + id: contentObject.id + }); + } + + public async removeFromAgenda(item: ViewItem): Promise { + return await this.httpService.delete(`/rest/agenda/item/${item.id}/`); + } + + public async create(item: Item): Promise { + throw new Error('Use `addItemToAgenda` for creations'); + } + + public async delete(item: ViewItem): Promise { + throw new Error('Use `removeFromAgenda` for deletions'); } /** diff --git a/client/src/app/core/repositories/base-repository.ts b/client/src/app/core/repositories/base-repository.ts index 6624da3a1..589e201c8 100644 --- a/client/src/app/core/repositories/base-repository.ts +++ b/client/src/app/core/repositories/base-repository.ts @@ -243,10 +243,10 @@ export abstract class BaseRepository { - if (!sendModel[key]) { + if (!sendModel[key] && sendModel[key] !== false) { delete sendModel[key]; } }); diff --git a/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html new file mode 100644 index 000000000..fc7b8b651 --- /dev/null +++ b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html @@ -0,0 +1,33 @@ + +
+ + Add to agenda + +
+ + + +
+ + + + {{ type.name | translate }} + + + +
+ + +
+ +
+
+
diff --git a/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.scss b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.spec.ts b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.spec.ts new file mode 100644 index 000000000..03dd39e6f --- /dev/null +++ b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.ts b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.ts new file mode 100644 index 000000000..26fc88988 --- /dev/null +++ b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.ts @@ -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; + + public constructor(private configService: ConfigService, private itemRepo: ItemRepositoryService) {} + + public ngOnInit(): void { + this.checkbox = this.form.controls.agenda_create as FormControl; + + this.configService.get('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(); + } +} diff --git a/client/src/app/shared/models/agenda/item.ts b/client/src/app/shared/models/agenda/item.ts index a819db49e..878107249 100644 --- a/client/src/app/shared/models/agenda/item.ts +++ b/client/src/app/shared/models/agenda/item.ts @@ -5,7 +5,7 @@ import { BaseModelWithContentObject } from '../base/base-model-with-content-obje * Determine visibility states for agenda items * Coming from "ConfigVariables" property "agenda_hide_internal_items_on_projector" */ -export const itemVisibilityChoices = [ +export const ItemVisibilityChoices = [ { key: 1, name: 'public', csvName: '' }, { key: 2, name: 'internal', csvName: 'internal' }, { key: 3, name: 'hidden', csvName: 'hidden' } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 4af80734d..82232db18 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -94,6 +94,7 @@ import { TileComponent } from './components/tile/tile.component'; import { BlockTileComponent } from './components/block-tile/block-tile.component'; import { IconContainerComponent } from './components/icon-container/icon-container.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. @@ -230,7 +231,8 @@ import { ListViewTableComponent } from './components/list-view-table/list-view-t SpeakerButtonComponent, PblNgridModule, PblNgridMaterialModule, - ListViewTableComponent + ListViewTableComponent, + AgendaContentObjectFormComponent ], declarations: [ PermsDirective, @@ -264,7 +266,8 @@ import { ListViewTableComponent } from './components/list-view-table/list-view-t TileComponent, BlockTileComponent, IconContainerComponent, - ListViewTableComponent + ListViewTableComponent, + AgendaContentObjectFormComponent ], providers: [ { provide: DateAdapter, useClass: OpenSlidesDateAdapter }, diff --git a/client/src/app/site/agenda/agenda-import.service.ts b/client/src/app/site/agenda/agenda-import.service.ts index 949b3b53e..4bf262c92 100644 --- a/client/src/app/site/agenda/agenda-import.service.ts +++ b/client/src/app/site/agenda/agenda-import.service.ts @@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service'; import { CreateTopic } from './models/create-topic'; 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 { ViewCreateTopic } from './models/view-create-topic'; @@ -162,13 +162,13 @@ export class AgendaImportService extends BaseImportService { if (!input) { return 1; // default, public item } else if (typeof input === 'string') { - const visibility = itemVisibilityChoices.find(choice => choice.csvName === input); + const visibility = ItemVisibilityChoices.find(choice => choice.csvName === input); if (visibility) { return visibility.key; } } else if (input === 1) { // 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) { return visibility.key; } diff --git a/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts b/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts index a3d8a53ad..561aaf382 100644 --- a/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts @@ -8,7 +8,7 @@ import { AgendaImportService } from '../../agenda-import.service'; import { BaseImportListComponent } from 'app/site/base/base-import-list'; import { CsvExportService } from 'app/core/ui-services/csv-export.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'; /** @@ -72,7 +72,7 @@ export class AgendaImportListComponent extends BaseImportListComponent choice.key === type); + const visibility = ItemVisibilityChoices.find(choice => choice.key === type); return visibility ? visibility.name : ''; } diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html index e11e4d6a7..e8b8b4bbf 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html @@ -175,9 +175,9 @@ - @@ -198,7 +198,12 @@ - + + diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts index fb058a898..9f00624e3 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts @@ -24,6 +24,8 @@ import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewItem } from '../../models/view-item'; import { ViewListOfSpeakers } from '../../models/view-list-of-speakers'; 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. @@ -123,7 +125,8 @@ export class AgendaListComponent extends ListViewBaseComponent impleme public filterService: AgendaFilterListService, private agendaPdfService: AgendaPdfService, private pdfService: PdfDocumentService, - private listOfSpeakersRepo: ListOfSpeakersRepositoryService + private listOfSpeakersRepo: ListOfSpeakersRepositoryService, + private topicRepo: TopicRepositoryService ) { super(titleService, translate, matSnackBar, storage); this.canMultiSelect = true; @@ -194,7 +197,7 @@ export class AgendaListComponent extends ListViewBaseComponent impleme */ public async onAutoNumbering(): Promise { 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); } } @@ -215,15 +218,26 @@ export class AgendaListComponent extends ListViewBaseComponent 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 { - const title = this.translate.instant('Are you sure you want to delete this entry?'); + public async removeFromAgenda(item: ViewItem): Promise { + const title = this.translate.instant('Are you sure you want to remove this entry from the agenda?'); const content = item.contentObject.getTitle(); 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 { + 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 impleme * Handler for deleting multiple entries. Needs items in selectedRows, which * is only filled with any data in multiSelect mode */ - public async deleteSelected(): Promise { - const title = this.translate.instant('Are you sure you want to delete all selected items?'); - if (await this.promptService.open(title, null)) { - for (const agenda of this.selectedRows) { - await this.repo.delete(agenda); + public async removeSelected(): Promise { + const title = this.translate.instant('Are you sure you want to remove all selected items from the agenda?'); + const content = this.translate.instant("All topics will be deleted and won't be accessible afterwards."); + if (await this.promptService.open(title, content)) { + 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 impleme * @param closed true if the item is to be considered done */ public async setClosedSelected(closed: boolean): Promise { - for (const agenda of this.selectedRows) { - await this.repo.update({ closed: closed }, agenda); + for (const item of this.selectedRows) { + await this.repo.update({ closed: closed }, item); } } @@ -259,8 +278,8 @@ export class AgendaListComponent extends ListViewBaseComponent impleme * @param visible true if the item is to be shown */ public async setAgendaType(agendaType: number): Promise { - for (const agenda of this.selectedRows) { - await this.repo.update({ type: agendaType }, agenda).then(null, this.raiseError); + for (const item of this.selectedRows) { + await this.repo.update({ type: agendaType }, item).then(null, this.raiseError); } } diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts index 834b668db..6e9dd2ed4 100644 --- a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts @@ -5,11 +5,11 @@ import { MatSnackBar } from '@angular/material'; import { BehaviorSubject, Observable } from 'rxjs'; 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 { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { ViewItem } from '../../models/view-item'; +import { ItemVisibilityChoices } from 'app/shared/models/agenda/item'; /** * Sort view for the agenda. @@ -30,7 +30,7 @@ export class AgendaSortComponent extends SortTreeViewComponent impleme * 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`. */ - public filterOptions: SortTreeFilterOption[] = itemVisibilityChoices.map(item => { + public filterOptions: SortTreeFilterOption[] = ItemVisibilityChoices.map(item => { return { label: item.name, id: item.key, state: false }; }); diff --git a/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts b/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts index e30a70a06..f555db05d 100644 --- a/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts +++ b/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; 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'; /** @@ -23,7 +23,7 @@ export class ItemInfoDialogComponent { /** * Hold item visibility */ - public itemVisibility = itemVisibilityChoices; + public itemVisibility = ItemVisibilityChoices; /** * Constructor diff --git a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts index d56dcf083..ede5805b3 100644 --- a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts +++ b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts @@ -402,7 +402,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit const title = this.translate.instant( '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); } } diff --git a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.ts b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.ts index 5d689bd28..7cad1089b 100644 --- a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.ts +++ b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.ts @@ -12,7 +12,7 @@ import { TopicRepositoryService } from 'app/core/repositories/topics/topic-repos import { ViewTopic } from '../../models/view-topic'; import { OperatorService } from 'app/core/core-services/operator.service'; 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 { Topic } from 'app/shared/models/topics/topic'; 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 */ - public itemVisibility = itemVisibilityChoices; + public itemVisibility = ItemVisibilityChoices; /** * Constructor for the topic detail page. diff --git a/client/src/app/site/agenda/models/view-item.ts b/client/src/app/site/agenda/models/view-item.ts index 9a2f44d76..0e1b7fa39 100644 --- a/client/src/app/site/agenda/models/view-item.ts +++ b/client/src/app/site/agenda/models/view-item.ts @@ -1,4 +1,4 @@ -import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item'; +import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item'; import { BaseViewModelWithAgendaItem, isBaseViewModelWithAgendaItem @@ -56,7 +56,7 @@ export class ViewItem extends BaseViewModelWithContentObject choice.key === this.type); + const type = ItemVisibilityChoices.find(choice => choice.key === this.type); return type ? type.name : ''; } @@ -68,7 +68,7 @@ export class ViewItem extends BaseViewModelWithContentObject choice.key === this.type); + const type = ItemVisibilityChoices.find(choice => choice.key === this.type); return type ? type.csvName : ''; } diff --git a/client/src/app/site/agenda/services/agenda-filter-list.service.ts b/client/src/app/site/agenda/services/agenda-filter-list.service.ts index 2b02997a8..2df30f042 100644 --- a/client/src/app/site/agenda/services/agenda-filter-list.service.ts +++ b/client/src/app/site/agenda/services/agenda-filter-list.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; 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 { StorageService } from 'app/core/core-services/storage.service'; @@ -60,7 +60,7 @@ export class AgendaFilterListService extends BaseFilterListService { * @returns a list of choices to filter from */ private createVisibilityFilterOptions(): OsFilterOption[] { - return itemVisibilityChoices.map(choice => ({ + return ItemVisibilityChoices.map(choice => ({ condition: choice.key as number, label: choice.name })); diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html index fcff271b9..a33cbc4af 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html @@ -33,6 +33,18 @@ + +
+ + +
+
@@ -263,18 +275,7 @@ >
- -
- -
+
diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts index a4df90e3a..b17ad0a3f 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts @@ -161,7 +161,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * @param repo * @param userRepo * @param pollService - * @param agendaRepo + * @param itemRepo * @param tagRepo * @param promptService */ @@ -177,7 +177,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn public repo: AssignmentRepositoryService, private userRepo: UserRepositoryService, public pollService: AssignmentPollService, - private agendaRepo: ItemRepositoryService, + private itemRepo: ItemRepositoryService, private tagRepo: TagRepositoryService, private promptService: PromptService, private pdfService: AssignmentPdfExportService, @@ -200,7 +200,9 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn description: '', poll_description_default: '', open_posts: 0, - agenda_item_id: '' // create agenda item + agenda_create: [''], + agenda_parent_id: [], + agenda_type: [''] }); this.candidatesForm = formBuilder.group({ userId: null @@ -212,7 +214,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn */ public ngOnInit(): void { this.getAssignmentByUrl(); - this.agendaObserver = this.agendaRepo.getViewModelListBehaviorSubject(); + this.agendaObserver = this.itemRepo.getViewModelListBehaviorSubject(); this.tagsObserver = this.tagRepo.getViewModelListBehaviorSubject(); this.mediafilesObserver = this.mediafileRepo.getViewModelListBehaviorSubject(); } @@ -292,7 +294,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn title: assignment.title || '', tags_id: assignment.assignment.tags_id || [], attachments_id: assignment.assignment.attachments_id || [], - agendaItem: assignment.assignment.agenda_item_id || null, phase: assignment.phase, // todo default: 0? description: assignment.assignment.description || '', poll_description_default: assignment.assignment.poll_description_default, @@ -516,4 +517,12 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn public getSanitizedText(text: string): SafeHtml { 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); + } } diff --git a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts index 609dbab85..f9cefc58a 100644 --- a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts @@ -114,7 +114,7 @@ export class AssignmentListComponent extends ListViewBaseComponent { 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) { await this.repo.delete(assignment); } diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts index 4ba766525..d9d4d2e74 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts @@ -157,7 +157,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit */ public async onDeletePoll(): Promise { 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); } } diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html index abde9a5a9..8f761d46d 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html @@ -99,10 +99,10 @@
- - + + + + +
+ +
+
@@ -791,28 +803,8 @@
- -
- - - - {{ type.name | translate }} - - - -
- - -
- +
+
diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts index 0ca5a92aa..08317ae68 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts @@ -13,8 +13,6 @@ import { ChangeRecommendationRepositoryService } from 'app/core/repositories/mot import { CreateMotion } from 'app/site/motions/models/create-motion'; import { ConfigService } from 'app/core/ui-services/config.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 { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service'; 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 { ViewCategory } from 'app/site/motions/models/view-category'; 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 { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; 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 { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.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 @@ -254,11 +252,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, */ public mediafilesObserver: BehaviorSubject; - /** - * Subject for agenda items - */ - public agendaItemObserver: BehaviorSubject; - /** * Subject for tags */ @@ -294,16 +287,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, */ 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 */ @@ -395,7 +378,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * @param dialogService For opening dialogs * @param el The native element * @param repo Motion Repository - * @param agendaRepo Read out agenda variables * @param changeRecoRepo Change Recommendation Repository * @param statuteRepo: Statute Paragraph Repository * @param mediafileRepo Mediafile Repository @@ -431,7 +413,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, private dialogService: MatDialog, private el: ElementRef, public repo: MotionRepositoryService, - private agendaRepo: ItemRepositoryService, private changeRecoRepo: ChangeRecommendationRepositoryService, private statuteRepo: StatuteParagraphRepositoryService, private configService: ConfigService, @@ -447,8 +428,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, private mediaFilerepo: MediafileRepositoryService, private workflowRepo: WorkflowRepositoryService, private blockRepo: MotionBlockRepositoryService, - private itemRepo: ItemRepositoryService, - private motionSortService: MotionSortListService + private motionSortService: MotionSortListService, + private itemRepo: ItemRepositoryService ) { super(title, translate, matSnackBar); } @@ -463,7 +444,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, this.mediafilesObserver = this.mediaFilerepo.getViewModelListBehaviorSubject(); this.workflowObserver = this.workflowRepo.getViewModelListBehaviorSubject(); this.blockObserver = this.blockRepo.getViewModelListBehaviorSubject(); - this.agendaItemObserver = this.itemRepo.getViewModelListBehaviorSubject(); this.motionObserver = this.repo.getViewModelListBehaviorSubject(); this.submitterObserver = 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 this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => { this.statuteParagraphs = newViewStatuteParagraphs; @@ -724,6 +697,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, reason: reason, category_id: [''], attachments_id: [[]], + agenda_create: [''], agenda_parent_id: [], agenda_type: [''], submitters_id: [], @@ -1092,7 +1066,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, const title = this.translate.instant( '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.setChangeRecoMode(ChangeRecoMode.ModifiedFinal), this.raiseError @@ -1111,7 +1085,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, */ public async deleteModifiedFinalVersion(): Promise { 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.updateMotion({ modified_final_version: '' }, this.motion).then( () => this.setChangeRecoMode(ChangeRecoMode.Final), @@ -1610,4 +1584,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, public editModifiedFinal(): void { 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); + } } diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts index d62c0114f..c654ec30c 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts @@ -119,7 +119,7 @@ export class MotionPollComponent implements OnInit { */ public async deletePoll(): Promise { 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); } } diff --git a/client/src/app/site/motions/services/motion-multiselect.service.ts b/client/src/app/site/motions/services/motion-multiselect.service.ts index 3987da382..d21be2174 100644 --- a/client/src/app/site/motions/services/motion-multiselect.service.ts +++ b/client/src/app/site/motions/services/motion-multiselect.service.ts @@ -71,7 +71,7 @@ export class MotionMultiselectService { */ public async delete(motions: ViewMotion[]): Promise { 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; for (const motion of motions) { diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 532522be5..a87375a11 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -280,7 +280,7 @@ export class UserListComponent extends ListViewBaseComponent implement */ public async deleteSelected(): Promise { 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) { await this.repo.delete(user); } diff --git a/openslides/agenda/config_variables.py b/openslides/agenda/config_variables.py index 17f87ebea..323792f28 100644 --- a/openslides/agenda/config_variables.py +++ b/openslides/agenda/config_variables.py @@ -30,6 +30,22 @@ def get_config_variables(): 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( name="agenda_numeral_system", default_value="arabic", diff --git a/openslides/agenda/mixins.py b/openslides/agenda/mixins.py index 6317588c2..db80a62ae 100644 --- a/openslides/agenda/mixins.py +++ b/openslides/agenda/mixins.py @@ -23,6 +23,9 @@ class AgendaItemMixin(models.Model): """ 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] = {} @@ -31,17 +34,20 @@ class AgendaItemMixin(models.Model): @property 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 - # the queryset. - return self.agenda_items.all()[0] + try: + return self.agenda_items.all()[0] + except IndexError: + return None @property def agenda_item_id(self): """ 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 def get_agenda_title_information(self): diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py index 84161538f..20dee81fa 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -4,7 +4,9 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db import models +from ..core.config import config from ..utils.autoupdate import inform_changed_data +from ..utils.rest_api import ValidationError 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 created: - attrs = {} - for attr in ("type", "parent_id", "comment", "duration", "weight"): - if instance.agenda_item_update_information.get(attr): - attrs[attr] = instance.agenda_item_update_information.get(attr) - Item.objects.create(content_object=instance, **attrs) - if not instance.agenda_item_skip_autoupdate: - instance_inform_changed_data = True + 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" - elif not instance.agenda_item_skip_autoupdate: + if should_create_item: + attrs = {} + for attr in ("type", "parent_id", "comment", "duration", "weight"): + if 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) + + if not instance.agenda_item_skip_autoupdate: + 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 + and instance.agenda_item is not None + ): # If the object has changed, then also the agenda item has to be sent. inform_changed_data(instance.agenda_item) diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index a525d1a34..fc27c0103 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -1,6 +1,7 @@ import jsonschema from django.contrib.auth import get_user_model from django.db import transaction +from django.db.utils import IntegrityError from openslides.core.config import config from openslides.utils.autoupdate import inform_changed_data @@ -15,10 +16,12 @@ from openslides.utils.rest_api import ( ValidationError, detail_route, list_route, + status, ) from openslides.utils.views import TreeSortMixin from ..utils.auth import has_perm +from ..utils.utils import get_model_from_collection_string from .access_permissions import ItemAccessPermissions from .models import Item, ListOfSpeakers, Speaker @@ -42,7 +45,14 @@ class ItemViewSet(ModelViewSet, TreeSortMixin): """ if self.action in ("list", "retrieve", "metadata"): 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 = ( has_perm(self.request.user, "agenda.can_see") and has_perm(self.request.user, "agenda.can_see_internal_items") @@ -56,6 +66,62 @@ class ItemViewSet(ModelViewSet, TreeSortMixin): result = False 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": , + "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): """ Customized view endpoint to update all children if the item type has changed. diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 958884040..611854083 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -8,6 +8,7 @@ from ..poll.serializers import default_votes_validator from ..utils.auth import get_group_model from ..utils.autoupdate import inform_changed_data from ..utils.rest_api import ( + BooleanField, CharField, DecimalField, DictField, @@ -69,6 +70,7 @@ class MotionBlockSerializer(ModelSerializer): Serializer for motion.models.Category objects. """ + agenda_create = BooleanField(write_only=True, required=False, allow_null=True) agenda_type = IntegerField( write_only=True, required=False, min_value=1, max_value=3, allow_null=True ) @@ -81,6 +83,7 @@ class MotionBlockSerializer(ModelSerializer): "title", "agenda_item_id", "list_of_speakers_id", + "agenda_create", "agenda_type", "agenda_parent_id", "internal", @@ -91,9 +94,11 @@ class MotionBlockSerializer(ModelSerializer): Customized create method. Set information about related agenda item into agenda_item_update_information container. """ + agenda_create = validated_data.pop("agenda_create", None) agenda_type = validated_data.pop("agenda_type", None) agenda_parent_id = validated_data.pop("agenda_parent_id", None) 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["parent_id"] = agenda_parent_id motion_block.save() @@ -417,6 +422,7 @@ class MotionSerializer(ModelSerializer): workflow_id = IntegerField( min_value=1, required=False, validators=[validate_workflow_field] ) + agenda_create = BooleanField(write_only=True, required=False, allow_null=True) agenda_type = IntegerField( write_only=True, required=False, min_value=1, max_value=3, allow_null=True ) @@ -456,6 +462,7 @@ class MotionSerializer(ModelSerializer): "polls", "agenda_item_id", "list_of_speakers_id", + "agenda_create", "agenda_type", "agenda_parent_id", "sort_parent", @@ -528,6 +535,9 @@ class MotionSerializer(ModelSerializer): motion.parent = validated_data.get("parent") motion.statute_paragraph = validated_data.get("statute_paragraph") 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( "agenda_type" ) diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index e2f1665ef..b29f12a27 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -19,6 +19,7 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.routers import DefaultRouter from rest_framework.serializers import ( + BooleanField, CharField, DecimalField, DictField, @@ -55,6 +56,7 @@ __all__ = [ "DestroyModelMixin", "CharField", "DictField", + "BooleanField", "FileField", "IntegerField", "JSONField", diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 80f7d8ea8..82a8fe03e 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -77,14 +77,20 @@ class TreeSortMixin: # layer) and a weight. nodes_to_check = [fake_root] # 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: node = nodes_to_check.pop() id = node["id"] if id is not None: # exclude the fake_root node[weight_key] = weight - weight += 1 + weight += 2 if id in ids_found: raise ValidationError(f"Duplicate id: {id}") if id not in all_model_ids: diff --git a/tests/integration/agenda/test_viewset.py b/tests/integration/agenda/test_viewset.py index 4b4215ea3..fc7280043 100644 --- a/tests/integration/agenda/test_viewset.py +++ b/tests/integration/agenda/test_viewset.py @@ -27,6 +27,8 @@ class ContentObjects(TestCase): lists of speakers. Asserts, that it is recognizes as a content object and tests creation and deletion of it and the related item and list of speaker. + Tests optional agenda items with motions, e.g. motion as a content + object without an item. """ def setUp(self): @@ -39,7 +41,15 @@ class ContentObjects(TestCase): def test_topic_is_list_of_speakers_content_object(self): 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") assert topic.agenda_item is not None @@ -51,7 +61,7 @@ class ContentObjects(TestCase): ) 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(") item_id = topic.agenda_item_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) + 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): """