diff --git a/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts b/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts index a0c528341..dd17723b1 100644 --- a/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts @@ -54,6 +54,14 @@ export class MotionCommentSectionRepositoryService extends BaseRepository< private http: HttpService ) { 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) => { @@ -109,4 +117,13 @@ export class MotionCommentSectionRepositoryService extends BaseRepository< private async deleteComment(motion: ViewMotion, section: ViewMotionCommentSection): Promise { 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 { + return await this.http.post('/rest/motions/motion-comment-section/sort/', { + ids: sections.map(section => section.id) + }); + } } diff --git a/client/src/app/shared/models/motions/motion-comment-section.ts b/client/src/app/shared/models/motions/motion-comment-section.ts index 0e4a3d304..715cee484 100644 --- a/client/src/app/shared/models/motions/motion-comment-section.ts +++ b/client/src/app/shared/models/motions/motion-comment-section.ts @@ -11,6 +11,7 @@ export class MotionCommentSection extends BaseModel { public name: string; public read_groups_id: number[]; public write_groups_id: number[]; + public weight: number; public constructor(input?: any) { super(MotionCommentSection.COLLECTIONSTRING, input); diff --git a/client/src/app/site/motions/models/view-motion-comment-section.ts b/client/src/app/site/motions/models/view-motion-comment-section.ts index d14b2a7f1..6de22db6f 100644 --- a/client/src/app/site/motions/models/view-motion-comment-section.ts +++ b/client/src/app/site/motions/models/view-motion-comment-section.ts @@ -48,6 +48,10 @@ export class ViewMotionCommentSection extends BaseViewModel

Comment fields

+ + +
@@ -139,3 +146,10 @@ + + + + diff --git a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-list.component.scss b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-list/motion-comment-section-list.component.scss similarity index 100% rename from client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-list.component.scss rename to client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-list/motion-comment-section-list.component.scss diff --git a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-list.component.spec.ts b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-list/motion-comment-section-list.component.spec.ts similarity index 100% rename from client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-list.component.spec.ts rename to client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-list/motion-comment-section-list.component.spec.ts diff --git a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-list.component.ts b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-list/motion-comment-section-list.component.ts similarity index 100% rename from client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-list.component.ts rename to client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-list/motion-comment-section-list.component.ts diff --git a/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.html b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.html new file mode 100644 index 000000000..d13edc176 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.html @@ -0,0 +1,13 @@ + + +
+

Sort Comments

+
+
+ + + + + + + diff --git a/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.scss b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.spec.ts b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.spec.ts new file mode 100644 index 000000000..a2d0bde5b --- /dev/null +++ b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.ts b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.ts new file mode 100644 index 000000000..f5b604f88 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-comment-section/components/motion-comment-section-sort/motion-comment-section-sort.component.ts @@ -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); + } +} diff --git a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-routing.module.ts b/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-routing.module.ts index 914a92a0c..e31328b1b 100644 --- a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-routing.module.ts +++ b/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section-routing.module.ts @@ -1,9 +1,13 @@ import { NgModule } from '@angular/core'; 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({ imports: [RouterModule.forChild(routes)], diff --git a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section.module.ts b/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section.module.ts index ad7b85c27..fb703e038 100644 --- a/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section.module.ts +++ b/client/src/app/site/motions/modules/motion-comment-section/motion-comment-section.module.ts @@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common'; import { MotionCommentSectionRoutingModule } from './motion-comment-section-routing.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({ - declarations: [MotionCommentSectionListComponent], + declarations: [MotionCommentSectionListComponent, MotionCommentSectionSortComponent], imports: [CommonModule, MotionCommentSectionRoutingModule, SharedModule] }) export class MotionCommentSectionModule {} diff --git a/openslides/motions/migrations/0029_motioncommentsection_weight.py b/openslides/motions/migrations/0029_motioncommentsection_weight.py new file mode 100644 index 000000000..5f09197c1 --- /dev/null +++ b/openslides/motions/migrations/0029_motioncommentsection_weight.py @@ -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), + ) + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 73be51e51..2aa48f76c 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -583,6 +583,11 @@ class MotionCommentSection(RESTModelMixin, models.Model): These groups have write-access to the section. """ + weight = models.IntegerField(default=10000) + """ + To sort comment sections. + """ + class Meta: default_permissions = () diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 49f399838..7be000671 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -373,7 +373,8 @@ class MotionCommentSectionSerializer(ModelSerializer): class Meta: 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): """ Call inform_changed_data on creation, so the cache includes the groups. """ diff --git a/openslides/motions/views.py b/openslides/motions/views.py index c0b24ec54..4b92f0971 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -4,6 +4,7 @@ import jsonschema from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError as DjangoValidationError from django.db import transaction +from django.db.models import Case, When from django.db.models.deletion import ProtectedError from django.http.request import QueryDict from rest_framework import status @@ -1228,7 +1229,7 @@ class MotionCommentSectionViewSet(ModelViewSet): """ if self.action in ("list", "retrieve"): 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( self.request.user, "motions.can_manage" ) @@ -1268,6 +1269,39 @@ class MotionCommentSectionViewSet(ModelViewSet): inform_changed_data(MotionComment.objects.filter(section=section)) 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: [, , ...] } + """ + # 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): """ diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index 2e60d00e2..7fccf8487 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -1331,6 +1331,77 @@ class TestMotionCommentSection(TestCase): 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