Merge pull request #4636 from FinnStutzenstein/assignmentAttachments

Add attachments to assignments
This commit is contained in:
Sean 2019-04-29 11:35:39 +02:00 committed by GitHub
commit 3dd659ae36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 146 additions and 12 deletions

View File

@ -42,7 +42,10 @@ export class ViewModelStoreService {
* @param collectionType The collection of the view model
* @param ids All ids to match
*/
public getMany<T extends BaseViewModel>(collectionType: ViewModelConstructor<T> | string, ids: number[]): T[] {
public getMany<T extends BaseViewModel>(collectionType: ViewModelConstructor<T> | string, ids?: number[]): T[] {
if (!ids) {
return [];
}
const repository = this.getRepository<T>(collectionType);
return ids

View File

@ -21,6 +21,8 @@ import { ViewUser } from 'app/site/users/models/view-user';
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
/**
* Repository Service for Assignments.
@ -56,7 +58,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
protected translate: TranslateService,
private httpService: HttpService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Assignment, [User, Item, Tag]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Assignment, [User, Item, Tag, Mediafile]);
}
public getAgendaTitle = (assignment: Partial<Assignment> | Partial<ViewAssignment>) => {
@ -74,6 +76,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
public createViewModel(assignment: Assignment): ViewAssignment {
const agendaItem = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id);
const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id);
const attachments = this.viewModelStoreService.getMany(ViewMediafile, assignment.attachments_id);
const assignmentRelatedUsers = this.createViewAssignmentRelatedUsers(assignment.assignment_related_users);
const assignmentPolls = this.createViewAssignmentPolls(assignment.polls);
@ -82,7 +85,8 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
assignmentRelatedUsers,
assignmentPolls,
agendaItem,
tags
tags,
attachments
);
viewAssignment.getVerboseName = this.getVerboseName;
viewAssignment.getAgendaTitle = () => this.getAgendaTitle(viewAssignment);

View File

@ -19,6 +19,7 @@ export class Assignment extends BaseModel<Assignment> {
public polls: AssignmentPoll[];
public agenda_item_id: number;
public tags_id: number[];
public attachments_id: number[];
public constructor(input?: any) {
super(Assignment.COLLECTIONSTRING, input);

View File

@ -40,10 +40,9 @@
<span translate>Attachments</span>:
<mat-list dense>
<mat-list-item *ngFor="let file of topic.attachments">
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.title }}</a>
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.getTitle() }}</a>
</mat-list-item>
</mat-list>
<!-- TODO: Mediafiles and attachments are not fully implemented -->
</h3>
</div>

View File

@ -98,6 +98,14 @@
</button>
</mat-menu>
</div>
<div *ngIf="assignment.attachments.length">
<span translate>Election documents</span>:
<mat-list dense>
<mat-list-item *ngFor="let file of assignment.attachments">
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.getTitle() }}</a>
</mat-list-item>
</mat-list>
</div>
</mat-card>
</ng-template>
@ -214,6 +222,7 @@
</mat-form-field>
</div>
<h4 translate>Description:</h4>
<!-- description: HTML Editor -->
<editor
formControlName="description"
@ -221,6 +230,7 @@
*ngIf="assignment && editAssignment"
required
></editor>
<!-- searchValueSelector: tags -->
<div class="content-field" *ngIf="tagsAvailable">
<os-search-value-selector
@ -234,6 +244,18 @@
></os-search-value-selector>
</div>
<!-- Attachments -->
<div class="content-field" *ngIf="mediafilesAvailable">
<os-search-value-selector
ngDefaultControl
[form]="assignmentForm"
[formControl]="assignmentForm.get('attachments_id')"
[multiple]="true"
listname="{{ 'Election documents' | translate }}"
[InputListValues]="mediafilesObserver"
></os-search-value-selector>
</div>
<!-- searchValueSelector: agendaItem -->
<div class="content-field" *ngIf="parentsAvailable">
<os-search-value-selector
@ -258,6 +280,7 @@
/>
</mat-form-field>
</div>
<!-- open posts: number -->
<div>
<mat-form-field>

View File

@ -25,6 +25,8 @@ import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewportService } from 'app/core/ui-services/viewport.service';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
/**
* Component for the assignment detail view
@ -77,6 +79,11 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
*/
public tagsObserver: BehaviorSubject<ViewTag[]>;
/**
* Used for the search value selector
*/
public mediafilesObserver: BehaviorSubject<ViewMediafile[]>;
/**
* Used in the search Value selector to assign an agenda item
*/
@ -133,7 +140,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
}
/**
* Checks if there are any tags available
* Checks if there are any mediafiles available
*/
public get mediafilesAvailable(): boolean {
return this.mediafilesObserver.getValue().length > 0;
}
/**
* Checks if there are any items available
*/
public get parentsAvailable(): boolean {
return this.agendaObserver.getValue().length > 0;
@ -174,7 +188,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
private agendaRepo: ItemRepositoryService,
private tagRepo: TagRepositoryService,
private promptService: PromptService,
private pdfService: AssignmentPdfExportService
private pdfService: AssignmentPdfExportService,
private mediafileRepo: MediafileRepositoryService
) {
super(title, translate, matSnackBar);
this.subscriptions.push(
@ -187,6 +202,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
this.assignmentForm = formBuilder.group({
phase: null,
tags_id: [],
attachments_id: [],
title: '',
description: '',
poll_description_default: '',
@ -205,6 +221,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
this.getAssignmentByUrl();
this.agendaObserver = this.agendaRepo.getViewModelListBehaviorSubject();
this.tagsObserver = this.tagRepo.getViewModelListBehaviorSubject();
this.mediafilesObserver = this.mediafileRepo.getViewModelListBehaviorSubject();
}
/**
@ -281,6 +298,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
this.assignmentForm.patchValue({
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 || '',

View File

@ -8,6 +8,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewAssignmentRelatedUser } from './view-assignment-related-user';
import { ViewAssignmentPoll } from './view-assignment-poll';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
/**
* A constant containing all possible assignment phases and their different
@ -40,6 +41,7 @@ export class ViewAssignment extends BaseAgendaViewModel {
private _assignmentPolls: ViewAssignmentPoll[];
private _agendaItem?: ViewItem;
private _tags?: ViewTag[];
private _attachments?: ViewMediafile[];
public get id(): number {
return this._assignment ? this._assignment.id : null;
@ -81,6 +83,10 @@ export class ViewAssignment extends BaseAgendaViewModel {
return this._tags || [];
}
public get attachments(): ViewMediafile[] {
return this._attachments || [];
}
/**
* unknown where the identifier to the phase is get
*/
@ -129,7 +135,8 @@ export class ViewAssignment extends BaseAgendaViewModel {
assignmentRelatedUsers: ViewAssignmentRelatedUser[],
assignmentPolls: ViewAssignmentPoll[],
agendaItem?: ViewItem,
tags?: ViewTag[]
tags?: ViewTag[],
attachments?: ViewMediafile[]
) {
super(Assignment.COLLECTIONSTRING);
@ -138,6 +145,7 @@ export class ViewAssignment extends BaseAgendaViewModel {
this._assignmentPolls = assignmentPolls;
this._agendaItem = agendaItem;
this._tags = tags;
this._attachments = attachments;
}
public updateDependencies(update: BaseViewModel): void {
@ -153,6 +161,13 @@ export class ViewAssignment extends BaseAgendaViewModel {
} else if (update instanceof ViewUser) {
this.assignmentRelatedUsers.forEach(aru => aru.updateDependencies(update));
this.polls.forEach(poll => poll.updateDependencies(update));
} else if (update instanceof ViewMediafile && this.assignment.attachments_id.includes(update.id)) {
const mediafileIndex = this._attachments.findIndex(_mediafile => _mediafile.id === update.id);
if (mediafileIndex < 0) {
this._attachments.push(update);
} else {
this._attachments[mediafileIndex] = update;
}
}
}

View File

@ -0,0 +1,19 @@
# Generated by Django 2.1.7 on 2019-04-26 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("mediafiles", "0003_auto_20190119_1425"),
("assignments", "0006_auto_20190119_1425"),
]
operations = [
migrations.AddField(
model_name="assignment",
name="attachments",
field=models.ManyToManyField(blank=True, to="mediafiles.Mediafile"),
)
]

View File

@ -10,6 +10,7 @@ from django.db import models
from openslides.agenda.models import Item, Speaker
from openslides.core.config import config
from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile
from openslides.poll.models import (
BaseOption,
BasePoll,
@ -78,7 +79,7 @@ class AssignmentManager(models.Manager):
polls are prefetched from the database.
"""
return self.get_queryset().prefetch_related(
"related_users", "agenda_items", "polls", "tags"
"related_users", "agenda_items", "polls", "tags", "attachments"
)
@ -141,6 +142,11 @@ class Assignment(RESTModelMixin, models.Model):
Tags for the assignment.
"""
attachments = models.ManyToManyField(Mediafile, blank=True)
"""
Mediafiles as attachments for this assignment.
"""
# In theory there could be one then more agenda_item. But we support only
# one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name="assignments")

View File

@ -206,6 +206,7 @@ class AssignmentFullSerializer(ModelSerializer):
"agenda_type",
"agenda_parent_id",
"tags",
"attachments",
)
validators = (posts_validator,)
@ -222,10 +223,12 @@ class AssignmentFullSerializer(ModelSerializer):
agenda_type = validated_data.pop("agenda_type", None)
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
tags = validated_data.pop("tags", [])
attachments = validated_data.pop("attachments", [])
assignment = Assignment(**validated_data)
assignment.agenda_item_update_information["type"] = agenda_type
assignment.agenda_item_update_information["parent_id"] = agenda_parent_id
assignment.save()
assignment.tags.add(*tags)
assignment.attachments.add(*attachments)
inform_changed_data(assignment)
return assignment

View File

@ -1,10 +1,13 @@
import pytest
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from openslides.assignments.models import Assignment
from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.test import TestCase
@ -19,16 +22,56 @@ def test_assignment_db_queries():
* 1 request to get all related users,
* 1 request to get the agenda item,
* 1 request to get the polls,
* 1 request to get the tags and
* 1 request to get the tags,
* 1 request to get the attachments and
* 10 request to fetch each related user again.
TODO: The last request are a bug.
TODO: The last requests are a bug.
"""
for index in range(10):
Assignment.objects.create(title=f"assignment{index}", open_posts=1)
assert count_queries(Assignment.get_elements) == 15
assert count_queries(Assignment.get_elements) == 16
class CreateAssignment(TestCase):
"""
Tests basic creation of assignments.
"""
def setUp(self):
self.client = APIClient()
self.client.login(username="admin", password="admin")
def test_simple(self):
response = self.client.post(
reverse("assignment-list"),
{"title": "test_title_ef3jpF)M329f30m)f82", "open_posts": 1},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
assignment = Assignment.objects.get()
self.assertEqual(assignment.title, "test_title_ef3jpF)M329f30m)f82")
def test_with_tags_and_mediafiles(self):
Tag.objects.create(name="test_tag")
Mediafile.objects.create(
title="test_file", mediafile=SimpleUploadedFile("title.txt", b"content")
)
response = self.client.post(
reverse("assignment-list"),
{
"title": "test_title_ef3jpF)M329f30m)f82",
"open_posts": 1,
"tags_id": [1],
"attachments_id": [1],
},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
assignment = Assignment.objects.get()
self.assertEqual(assignment.title, "test_title_ef3jpF)M329f30m)f82")
self.assertTrue(assignment.tags.exists())
self.assertTrue(assignment.attachments.exists())
class CanidatureSelf(TestCase):