Merge pull request #4636 from FinnStutzenstein/assignmentAttachments
Add attachments to assignments
This commit is contained in:
commit
3dd659ae36
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 || '',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
)
|
||||
]
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user