Merge pull request #4865 from FinnStutzenstein/MotionCommentSectionSorting

sorting of motion comment sections
This commit is contained in:
Finn Stutzenstein 2019-07-22 16:03:13 +02:00 committed by GitHub
commit 61002a6bad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 271 additions and 6 deletions

View File

@ -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)
});
}
} }

View File

@ -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);

View File

@ -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.
*/ */

View File

@ -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>

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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)],

View File

@ -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 {}

View File

@ -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),
)
]

View File

@ -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 = ()

View File

@ -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. """

View File

@ -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):
""" """

View File

@ -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.