Merge pull request #4865 from FinnStutzenstein/MotionCommentSectionSorting
sorting of motion comment sections
This commit is contained in:
commit
61002a6bad
@ -54,6 +54,14 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
|
|||||||
private http: HttpService
|
private http: HttpService
|
||||||
) {
|
) {
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionCommentSection, [Group]);
|
super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionCommentSection, [Group]);
|
||||||
|
|
||||||
|
this.viewModelSortFn = (a: ViewMotionCommentSection, b: ViewMotionCommentSection) => {
|
||||||
|
if (a.weight === b.weight) {
|
||||||
|
return a.id - b.id;
|
||||||
|
} else {
|
||||||
|
return a.weight - b.weight;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTitle = (titleInformation: MotionCommentSectionTitleInformation) => {
|
public getTitle = (titleInformation: MotionCommentSectionTitleInformation) => {
|
||||||
@ -109,4 +117,13 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
|
|||||||
private async deleteComment(motion: ViewMotion, section: ViewMotionCommentSection): Promise<void> {
|
private async deleteComment(motion: ViewMotion, section: ViewMotionCommentSection): Promise<void> {
|
||||||
return await this.http.delete(`/rest/motions/motion/${motion.id}/manage_comments/`, { section_id: section.id });
|
return await this.http.delete(`/rest/motions/motion/${motion.id}/manage_comments/`, { section_id: section.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort all comment sections. All sections must be given excatly once.
|
||||||
|
*/
|
||||||
|
public async sortCommentSections(sections: ViewMotionCommentSection[]): Promise<void> {
|
||||||
|
return await this.http.post('/rest/motions/motion-comment-section/sort/', {
|
||||||
|
ids: sections.map(section => section.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ export class MotionCommentSection extends BaseModel<MotionCommentSection> {
|
|||||||
public name: string;
|
public name: string;
|
||||||
public read_groups_id: number[];
|
public read_groups_id: number[];
|
||||||
public write_groups_id: number[];
|
public write_groups_id: number[];
|
||||||
|
public weight: number;
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(MotionCommentSection.COLLECTIONSTRING, input);
|
super(MotionCommentSection.COLLECTIONSTRING, input);
|
||||||
|
@ -48,6 +48,10 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
|
|||||||
return this._writeGroups;
|
return this._writeGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get weight(): number {
|
||||||
|
return this.section.weight;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Where is this needed? Try to avoid this.
|
* TODO: Where is this needed? Try to avoid this.
|
||||||
*/
|
*/
|
||||||
|
@ -3,6 +3,13 @@
|
|||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Comment fields</h2>
|
<h2 translate>Comment fields</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<div class="menu-slot">
|
||||||
|
<button type="button" mat-icon-button [matMenuTriggerFor]="commentListMenu">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<div class="head-spacer"></div>
|
<div class="head-spacer"></div>
|
||||||
@ -139,3 +146,10 @@
|
|||||||
</mat-action-row>
|
</mat-action-row>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
||||||
|
<mat-menu #commentListMenu="matMenu">
|
||||||
|
<button mat-menu-item routerLink="sort">
|
||||||
|
<mat-icon>sort</mat-icon>
|
||||||
|
<span translate>Sort</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
@ -0,0 +1,13 @@
|
|||||||
|
<os-head-bar [nav]="false">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot">
|
||||||
|
<h2 translate>Sort Comments</h2>
|
||||||
|
</div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<mat-card class="os-form-card">
|
||||||
|
<!-- The sorting component -->
|
||||||
|
<os-sorting-list (sortEvent)="onSortingChange($event)" [live]="true" [input]="comments" #sorter>
|
||||||
|
</os-sorting-list>
|
||||||
|
</mat-card>
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MotionCommentSectionSortComponent } from './motion-comment-section-sort.component';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('MotionCommentSectionSortComponent', () => {
|
||||||
|
let component: MotionCommentSectionSortComponent;
|
||||||
|
let fixture: ComponentFixture<MotionCommentSectionSortComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [MotionCommentSectionSortComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MotionCommentSectionSortComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
|
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
|
||||||
|
import { ViewMotionCommentSection } from 'app/site/motions/models/view-motion-comment-section';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorting view for motion comments
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-motion-comment-section-sort',
|
||||||
|
templateUrl: './motion-comment-section-sort.component.html',
|
||||||
|
styleUrls: ['./motion-comment-section-sort.component.scss']
|
||||||
|
})
|
||||||
|
export class MotionCommentSectionSortComponent extends BaseViewComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Holds the models
|
||||||
|
*/
|
||||||
|
public comments: ViewMotionCommentSection[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param title Title service
|
||||||
|
* @param translate Translate service
|
||||||
|
* @param snackBar Snack bar
|
||||||
|
* @param repo Motion comment repository service
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
title: Title,
|
||||||
|
translate: TranslateService, // protected required for ng-translate-extract
|
||||||
|
snackBar: MatSnackBar,
|
||||||
|
private repo: MotionCommentSectionRepositoryService
|
||||||
|
) {
|
||||||
|
super(title, translate, snackBar);
|
||||||
|
super.setTitle('Sort comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view models from the repo
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.repo.getViewModelListObservable().subscribe(comments => (this.comments = comments));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed if the sorting changes
|
||||||
|
*
|
||||||
|
* @param commentsInOrder
|
||||||
|
*/
|
||||||
|
public onSortingChange(commentsInOrder: ViewMotionCommentSection[]): void {
|
||||||
|
this.repo.sortCommentSections(commentsInOrder).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { MotionCommentSectionListComponent } from './motion-comment-section-list.component';
|
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
|
||||||
|
import { MotionCommentSectionSortComponent } from './components/motion-comment-section-sort/motion-comment-section-sort.component';
|
||||||
|
|
||||||
const routes: Routes = [{ path: '', component: MotionCommentSectionListComponent, pathMatch: 'full' }];
|
const routes: Routes = [
|
||||||
|
{ path: '', component: MotionCommentSectionListComponent, pathMatch: 'full' },
|
||||||
|
{ path: 'sort', component: MotionCommentSectionSortComponent }
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
|
|||||||
|
|
||||||
import { MotionCommentSectionRoutingModule } from './motion-comment-section-routing.module';
|
import { MotionCommentSectionRoutingModule } from './motion-comment-section-routing.module';
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
import { MotionCommentSectionListComponent } from './motion-comment-section-list.component';
|
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
|
||||||
|
import { MotionCommentSectionSortComponent } from './components/motion-comment-section-sort/motion-comment-section-sort.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [MotionCommentSectionListComponent],
|
declarations: [MotionCommentSectionListComponent, MotionCommentSectionSortComponent],
|
||||||
imports: [CommonModule, MotionCommentSectionRoutingModule, SharedModule]
|
imports: [CommonModule, MotionCommentSectionRoutingModule, SharedModule]
|
||||||
})
|
})
|
||||||
export class MotionCommentSectionModule {}
|
export class MotionCommentSectionModule {}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 2.2.3 on 2019-07-19 10:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("motions", "0028_subcategories")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motioncommentsection",
|
||||||
|
name="weight",
|
||||||
|
field=models.IntegerField(default=10000),
|
||||||
|
)
|
||||||
|
]
|
@ -583,6 +583,11 @@ class MotionCommentSection(RESTModelMixin, models.Model):
|
|||||||
These groups have write-access to the section.
|
These groups have write-access to the section.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
weight = models.IntegerField(default=10000)
|
||||||
|
"""
|
||||||
|
To sort comment sections.
|
||||||
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
|
@ -373,7 +373,8 @@ class MotionCommentSectionSerializer(ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionCommentSection
|
model = MotionCommentSection
|
||||||
fields = ("id", "name", "read_groups", "write_groups")
|
fields = ("id", "name", "read_groups", "write_groups", "weight")
|
||||||
|
read_only_fields = ("weight",)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
""" Call inform_changed_data on creation, so the cache includes the groups. """
|
""" Call inform_changed_data on creation, so the cache includes the groups. """
|
||||||
|
@ -4,6 +4,7 @@ import jsonschema
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Case, When
|
||||||
from django.db.models.deletion import ProtectedError
|
from django.db.models.deletion import ProtectedError
|
||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -1228,7 +1229,7 @@ class MotionCommentSectionViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action in ("create", "destroy", "update", "partial_update"):
|
elif self.action in ("create", "destroy", "update", "partial_update", "sort"):
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage"
|
self.request.user, "motions.can_manage"
|
||||||
)
|
)
|
||||||
@ -1268,6 +1269,39 @@ class MotionCommentSectionViewSet(ModelViewSet):
|
|||||||
inform_changed_data(MotionComment.objects.filter(section=section))
|
inform_changed_data(MotionComment.objects.filter(section=section))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@list_route(methods=["post"])
|
||||||
|
def sort(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Changes the sorting of comment sections. Every id must be given exactly once.
|
||||||
|
Expected data: { ids: [<id>, <id>, ...] }
|
||||||
|
"""
|
||||||
|
# Check request data format
|
||||||
|
ids = request.data.get("ids")
|
||||||
|
if not isinstance(ids, list):
|
||||||
|
raise ValidationError({"detail": "ids must be a list"})
|
||||||
|
for id in ids:
|
||||||
|
if not isinstance(id, int):
|
||||||
|
raise ValidationError({"detail": "every id must be an int"})
|
||||||
|
|
||||||
|
# Validate, that every id is given exactly once.
|
||||||
|
ids_set = set(ids)
|
||||||
|
if len(ids_set) != len(ids):
|
||||||
|
raise ValidationError({"detail": "only unique ids are expected"})
|
||||||
|
db_ids_set = set(
|
||||||
|
list(MotionCommentSection.objects.all().values_list(flat=True))
|
||||||
|
)
|
||||||
|
if ids_set != db_ids_set:
|
||||||
|
raise ValidationError({"detail": "every id must be given"})
|
||||||
|
|
||||||
|
# Ids are ok.
|
||||||
|
preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(ids)])
|
||||||
|
queryset = MotionCommentSection.objects.filter(pk__in=ids).order_by(preserved)
|
||||||
|
for index, section in enumerate(queryset):
|
||||||
|
section.weight = index + 1
|
||||||
|
section.save()
|
||||||
|
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class StatuteParagraphViewSet(ModelViewSet):
|
class StatuteParagraphViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -1331,6 +1331,77 @@ class TestMotionCommentSection(TestCase):
|
|||||||
self.assertEqual(MotionCommentSection.objects.count(), 1)
|
self.assertEqual(MotionCommentSection.objects.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMotionCommentSectionSorting(TestCase):
|
||||||
|
"""
|
||||||
|
Tests sorting of comment sections.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.section1 = MotionCommentSection(name="test_name_hponzp<zp7NUJKLAykbX")
|
||||||
|
self.section1.save()
|
||||||
|
self.section2 = MotionCommentSection(name="test_name_eix,b<bojbP'JO;<kVKL")
|
||||||
|
self.section2.save()
|
||||||
|
self.section3 = MotionCommentSection(name="test_name_ojMOeigSIOfhmpouweqc")
|
||||||
|
self.section3.save()
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motioncommentsection-sort"), {"ids": [3, 2, 1]}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
section1 = MotionCommentSection.objects.get(pk=1)
|
||||||
|
self.assertEqual(section1.weight, 3)
|
||||||
|
section2 = MotionCommentSection.objects.get(pk=2)
|
||||||
|
self.assertEqual(section2.weight, 2)
|
||||||
|
section3 = MotionCommentSection.objects.get(pk=3)
|
||||||
|
self.assertEqual(section3.weight, 1)
|
||||||
|
|
||||||
|
def test_wrong_data(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motioncommentsection-sort"), {"ids": "some_string"}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assert_not_changed()
|
||||||
|
|
||||||
|
def test_wrong_id_type(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motioncommentsection-sort"),
|
||||||
|
{"ids": [1, 2, "some_string"]},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assert_not_changed()
|
||||||
|
|
||||||
|
def test_missing_id(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motioncommentsection-sort"), {"ids": [3, 1]}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assert_not_changed()
|
||||||
|
|
||||||
|
def test_duplicate_id(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motioncommentsection-sort"), {"ids": [3, 2, 1, 1]}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assert_not_changed()
|
||||||
|
|
||||||
|
def test_wrong_id(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motioncommentsection-sort"), {"ids": [3, 4, 1]}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assert_not_changed()
|
||||||
|
|
||||||
|
def assert_not_changed(self):
|
||||||
|
""" Asserts, that every comment section has the default weight of 10000. """
|
||||||
|
for section in MotionCommentSection.objects.all():
|
||||||
|
self.assertEqual(section.weight, 10000)
|
||||||
|
|
||||||
|
|
||||||
class RetrieveMotionChangeRecommendation(TestCase):
|
class RetrieveMotionChangeRecommendation(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests retrieving motion change recommendations.
|
Tests retrieving motion change recommendations.
|
||||||
|
Loading…
Reference in New Issue
Block a user