diff --git a/client/src/app/core/core-services/view-model-store.service.ts b/client/src/app/core/core-services/view-model-store.service.ts index b69a6de34..7d1a56b49 100644 --- a/client/src/app/core/core-services/view-model-store.service.ts +++ b/client/src/app/core/core-services/view-model-store.service.ts @@ -42,7 +42,10 @@ export class ViewModelStoreService { * @param collectionType The collection of the view model * @param ids All ids to match */ - public getMany(collectionType: ViewModelConstructor | string, ids: number[]): T[] { + public getMany(collectionType: ViewModelConstructor | string, ids?: number[]): T[] { + if (!ids) { + return []; + } const repository = this.getRepository(collectionType); return ids diff --git a/client/src/app/core/repositories/assignments/assignment-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-repository.service.ts index cc6943eda..f3cf68666 100644 --- a/client/src/app/core/repositories/assignments/assignment-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-repository.service.ts @@ -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 | Partial) => { @@ -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); diff --git a/client/src/app/shared/models/assignments/assignment.ts b/client/src/app/shared/models/assignments/assignment.ts index f7b3bf9e6..f2db1d103 100644 --- a/client/src/app/shared/models/assignments/assignment.ts +++ b/client/src/app/shared/models/assignments/assignment.ts @@ -19,6 +19,7 @@ export class Assignment extends BaseModel { public polls: AssignmentPoll[]; public agenda_item_id: number; public tags_id: number[]; + public attachments_id: number[]; public constructor(input?: any) { super(Assignment.COLLECTIONSTRING, input); diff --git a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html index c1658ffc7..9804e9f22 100644 --- a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html +++ b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html @@ -40,10 +40,9 @@ Attachments: - {{ file.title }} + {{ file.getTitle() }} - 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 c3f0e5dd5..cbb9e8a36 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 @@ -98,6 +98,14 @@ +
+ Election documents: + + + {{ file.getTitle() }} + + +
@@ -214,6 +222,7 @@

Description:

+ +
+ +
+ +
+
+
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 0649fabfe..6402bbcab 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 @@ -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; + /** + * Used for the search value selector + */ + public mediafilesObserver: BehaviorSubject; + /** * 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 || '', diff --git a/client/src/app/site/assignments/models/view-assignment.ts b/client/src/app/site/assignments/models/view-assignment.ts index 3c0fd5bd4..0b5516ba0 100644 --- a/client/src/app/site/assignments/models/view-assignment.ts +++ b/client/src/app/site/assignments/models/view-assignment.ts @@ -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; + } } } diff --git a/openslides/assignments/migrations/0007_assignment_attachments.py b/openslides/assignments/migrations/0007_assignment_attachments.py new file mode 100644 index 000000000..432b1bced --- /dev/null +++ b/openslides/assignments/migrations/0007_assignment_attachments.py @@ -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"), + ) + ] diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index fefe77a9a..a65f2509f 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -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") diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index 7953217de..75470da9b 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -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 diff --git a/tests/integration/assignments/test_viewset.py b/tests/integration/assignments/test_viewset.py index e75e365fc..4031e8817 100644 --- a/tests/integration/assignments/test_viewset.py +++ b/tests/integration/assignments/test_viewset.py @@ -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):