Add tags for agenda items
Adds tags for agnda items, adds tag filter in agenda list view, server changes, client relations, adjust agenda csv exporter
This commit is contained in:
parent
0ee70b7434
commit
b6bb1fe767
@ -20,6 +20,7 @@ import {
|
||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewTopic } from 'app/site/topics/models/view-topic';
|
||||
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
|
||||
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
|
||||
@ -34,6 +35,12 @@ const ItemRelations: RelationDefinition[] = [
|
||||
VForeignVerbose: 'BaseViewModelWithAgendaItem',
|
||||
ownContentObjectDataKey: 'contentObjectData',
|
||||
ownKey: 'contentObject'
|
||||
},
|
||||
{
|
||||
type: 'M2M',
|
||||
ownIdKey: 'tags_id',
|
||||
ownKey: 'tags',
|
||||
foreignViewModel: ViewTag
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -49,6 +49,7 @@ export class Item extends BaseModelWithContentObject<Item> {
|
||||
public weight: number;
|
||||
public parent_id: number;
|
||||
public level: number;
|
||||
public tags_id: number[];
|
||||
|
||||
public constructor(input?: any) {
|
||||
super(Item.COLLECTIONSTRING, input);
|
||||
|
@ -33,7 +33,6 @@
|
||||
<div *pblNgridCellDef="'title'; row as item; rowContext as rowContext" class="cell-slot fill">
|
||||
<a class="detail-link" [routerLink]="getDetailUrl(item)" *ngIf="!isMultiSelect"></a>
|
||||
<div [ngStyle]="{ 'margin-left': item.level * 25 + 'px' }" class="innerTable">
|
||||
|
||||
<!-- Title line -->
|
||||
<div class="title-line ellipsis-overflow">
|
||||
<!-- Is Closed -->
|
||||
@ -64,9 +63,22 @@
|
||||
<!-- Info Column -->
|
||||
<div *pblNgridCellDef="'info'; row as item" class="cell-slot fill clickable" (click)="openEditInfo(item)">
|
||||
<div class="info-col-items">
|
||||
<div *osPerms="'agenda.can_manage'; and: item.verboseType">
|
||||
<!-- Tags -->
|
||||
<div *ngIf="item.tags && item.tags.length">
|
||||
<os-icon-container icon="local_offer">
|
||||
<span *ngFor="let tag of item.tags; let last = last">
|
||||
{{ tag.getTitle() }}
|
||||
<span *ngIf="!last">, </span>
|
||||
</span>
|
||||
</os-icon-container>
|
||||
</div>
|
||||
|
||||
<!-- Visibility (Internal, Public, Hidden) -->
|
||||
<div *osPerms="'agenda.can_manage'; and: item.verboseType" class="spacer-top-5">
|
||||
<os-icon-container icon="visibility">{{ item.verboseType | translate }}</os-icon-container>
|
||||
</div>
|
||||
|
||||
<!-- Duration -->
|
||||
<div *ngIf="item.duration" class="spacer-top-5">
|
||||
<os-icon-container icon="access_time">
|
||||
{{ durationService.durationToString(item.duration, 'h') }}
|
||||
@ -98,6 +110,8 @@
|
||||
<span>{{ 'Multiselect' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- automatic numbering -->
|
||||
<button mat-menu-item *ngIf="isNumberingAllowed" (click)="onAutoNumbering()">
|
||||
<mat-icon>format_list_numbered</mat-icon>
|
||||
@ -117,6 +131,15 @@
|
||||
<mat-icon>mic</mat-icon>
|
||||
<span>{{ 'Current list of speakers' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Tags -->
|
||||
<button mat-menu-item routerLink="/tags" *osPerms="'core.can_manage_tags'">
|
||||
<mat-icon>local_offer</mat-icon>
|
||||
<span>{{ 'Tags' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- CSV export -->
|
||||
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="csvExportItemList()">
|
||||
<mat-icon>archive</mat-icon>
|
||||
|
@ -1,6 +1,15 @@
|
||||
<h1 mat-dialog-title *ngIf="item">{{ 'Edit details for' | translate }} {{ item.getTitle() }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<form class="item-dialog-form" [formGroup]="agendaInfoForm" (keydown)="onKeyDown($event)">
|
||||
<!-- Tag -->
|
||||
<mat-form-field *ngIf="isTagAvailable()">
|
||||
<mat-select formControlName="tags_id" multiple placeholder="{{ 'Tags' | translate }}">
|
||||
<mat-option *ngFor="let tag of tags" [value]="tag.id">
|
||||
{{ tag.getTitle() | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Visibility -->
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="type" placeholder="{{ 'Agenda visibility' | translate }}">
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
import { durationValidator } from 'app/shared/validators/custom-validators';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewItem } from '../../models/view-item';
|
||||
|
||||
/**
|
||||
@ -15,7 +17,7 @@ import { ViewItem } from '../../models/view-item';
|
||||
templateUrl: './item-info-dialog.component.html',
|
||||
styleUrls: ['./item-info-dialog.component.scss']
|
||||
})
|
||||
export class ItemInfoDialogComponent {
|
||||
export class ItemInfoDialogComponent implements OnInit {
|
||||
/**
|
||||
* Holds the agenda item form
|
||||
*/
|
||||
@ -26,6 +28,8 @@ export class ItemInfoDialogComponent {
|
||||
*/
|
||||
public itemVisibility = ItemVisibilityChoices;
|
||||
|
||||
public tags: ViewTag[] = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -38,22 +42,42 @@ export class ItemInfoDialogComponent {
|
||||
public formBuilder: FormBuilder,
|
||||
public durationService: DurationService,
|
||||
public dialogRef: MatDialogRef<ItemInfoDialogComponent>,
|
||||
public tagRepo: TagRepositoryService,
|
||||
@Inject(MAT_DIALOG_DATA) public item: ViewItem
|
||||
) {
|
||||
this.agendaInfoForm = this.formBuilder.group({
|
||||
tags_id: [],
|
||||
type: [''],
|
||||
durationText: ['', durationValidator],
|
||||
item_number: [''],
|
||||
comment: ['']
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
// load current values
|
||||
if (item) {
|
||||
this.agendaInfoForm.get('type').setValue(item.type);
|
||||
this.agendaInfoForm.get('durationText').setValue(this.durationService.durationToString(item.duration, 'h'));
|
||||
this.agendaInfoForm.get('item_number').setValue(item.item_number);
|
||||
this.agendaInfoForm.get('comment').setValue(item.comment);
|
||||
if (this.item) {
|
||||
this.agendaInfoForm.get('tags_id').setValue(this.item.tags_id);
|
||||
this.agendaInfoForm.get('type').setValue(this.item.type);
|
||||
this.agendaInfoForm
|
||||
.get('durationText')
|
||||
.setValue(this.durationService.durationToString(this.item.duration, 'h'));
|
||||
this.agendaInfoForm.get('item_number').setValue(this.item.item_number);
|
||||
this.agendaInfoForm.get('comment').setValue(this.item.comment);
|
||||
}
|
||||
|
||||
this.tagRepo.getViewModelListObservable().subscribe(tags => {
|
||||
this.tags = tags;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if tags are available.
|
||||
*
|
||||
* @returns A boolean if they are available.
|
||||
*/
|
||||
public isTagAvailable(): boolean {
|
||||
return !!this.tags && this.tags.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
import { ContentObject } from 'app/shared/models/base/content-object';
|
||||
import { BaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
|
||||
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
|
||||
export interface ItemTitleInformation {
|
||||
contentObject: BaseViewModelWithAgendaItem;
|
||||
@ -53,4 +54,7 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
|
||||
return this.contentObjectData.collection;
|
||||
}
|
||||
}
|
||||
export interface ViewItem extends Item {}
|
||||
interface IItemRelations {
|
||||
tags: ViewTag[];
|
||||
}
|
||||
export interface ViewItem extends Item, IItemRelations {}
|
||||
|
@ -36,7 +36,8 @@ export class AgendaCsvExportService {
|
||||
},
|
||||
{ label: 'Duration', property: 'duration' },
|
||||
{ label: 'Comment', property: 'comment' },
|
||||
{ label: 'Item type', property: 'verboseCsvType' }
|
||||
{ label: 'Item type', property: 'verboseCsvType' },
|
||||
{ label: 'Tags', property: 'tags' }
|
||||
],
|
||||
this.translate.instant('Agenda') + '.csv'
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
import { ViewItem } from '../models/view-item';
|
||||
@ -20,14 +21,27 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
|
||||
*/
|
||||
protected storageKey = 'AgendaList';
|
||||
|
||||
public tagFilterOptions: OsFilter = {
|
||||
property: 'tags_id',
|
||||
label: 'Tags',
|
||||
options: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor. Also creates the dynamic filter options
|
||||
*
|
||||
* @param store
|
||||
* @param translate Translation service
|
||||
*/
|
||||
public constructor(store: StorageService, OSStatus: OpenSlidesStatusService, private translate: TranslateService) {
|
||||
public constructor(
|
||||
store: StorageService,
|
||||
OSStatus: OpenSlidesStatusService,
|
||||
private translate: TranslateService,
|
||||
tagRepo: TagRepositoryService
|
||||
) {
|
||||
super(store, OSStatus);
|
||||
|
||||
this.updateFilterForRepo(tagRepo, this.tagFilterOptions, this.translate.instant('No tags'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,11 +49,6 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
|
||||
*/
|
||||
protected getFilterDefinitions(): OsFilter[] {
|
||||
return [
|
||||
{
|
||||
label: 'Visibility',
|
||||
property: 'type',
|
||||
options: this.createVisibilityFilterOptions()
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
property: 'closed',
|
||||
@ -48,6 +57,12 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
|
||||
{ label: this.translate.instant('Closed items'), condition: true }
|
||||
]
|
||||
},
|
||||
this.tagFilterOptions,
|
||||
{
|
||||
label: 'Visibility',
|
||||
property: 'type',
|
||||
options: this.createVisibilityFilterOptions()
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
property: 'collection',
|
||||
|
19
openslides/agenda/migrations/0009_item_tags.py
Normal file
19
openslides/agenda/migrations/0009_item_tags.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.12 on 2020-05-14 10:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0031_projector_default_height"),
|
||||
("agenda", "0008_default_ordering_item"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="tags",
|
||||
field=models.ManyToManyField(blank=True, to="core.Tag"),
|
||||
),
|
||||
]
|
@ -9,7 +9,7 @@ from django.db import models, transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from openslides.core.config import config
|
||||
from openslides.core.models import Countdown
|
||||
from openslides.core.models import Countdown, Tag
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
from openslides.utils.manager import BaseManager
|
||||
@ -41,7 +41,7 @@ class ItemManager(BaseManager):
|
||||
return (
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.prefetch_related("content_object", "parent")
|
||||
.prefetch_related("content_object", "parent", "tags")
|
||||
)
|
||||
|
||||
def get_only_non_public_items(self):
|
||||
@ -276,6 +276,11 @@ class Item(RESTModelMixin, models.Model):
|
||||
Field for generic relation to a related object. General field to the related object.
|
||||
"""
|
||||
|
||||
tags = models.ManyToManyField(Tag, blank=True)
|
||||
"""
|
||||
Tags for the agenda item.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
|
@ -51,6 +51,7 @@ class ItemSerializer(ModelSerializer):
|
||||
"weight",
|
||||
"parent",
|
||||
"level",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
|
@ -32,6 +32,7 @@ def test_agenda_item_db_queries():
|
||||
* 1 request to get all topics,
|
||||
* 1 request to get all motion blocks and
|
||||
* 1 request to get all parents
|
||||
* 1 request to get all tags
|
||||
"""
|
||||
parent = Topic.objects.create(title="parent").agenda_item
|
||||
for index in range(10):
|
||||
@ -45,7 +46,7 @@ def test_agenda_item_db_queries():
|
||||
MotionBlock.objects.create(title="block1")
|
||||
MotionBlock.objects.create(title="block1")
|
||||
|
||||
assert count_queries(Item.get_elements)() == 6
|
||||
assert count_queries(Item.get_elements)() == 7
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=False)
|
||||
|
@ -109,7 +109,7 @@ class CreateMotion(TestCase):
|
||||
The created motion should have an identifier and the admin user should
|
||||
be the submitter.
|
||||
"""
|
||||
with self.assertNumQueries(51):
|
||||
with self.assertNumQueries(52):
|
||||
response = self.client.post(
|
||||
reverse("motion-list"),
|
||||
{
|
||||
@ -185,6 +185,7 @@ class CreateMotion(TestCase):
|
||||
"weight": 10000,
|
||||
"parent_id": None,
|
||||
"level": 0,
|
||||
"tags_id": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user