Merge pull request #3940 from CatoTH/OpenSlides-3-ChangeRecommendations-internal

Internal change recommendations
This commit is contained in:
Sean 2018-10-30 10:02:04 +01:00 committed by GitHub
commit eeb29d140b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 7 deletions

View File

@ -8,6 +8,7 @@ export class MotionChangeReco extends BaseModel<MotionChangeReco> {
public id: number; public id: number;
public motion_id: number; public motion_id: number;
public rejected: boolean; public rejected: boolean;
public internal: boolean;
public type: number; public type: number;
public other_description: string; public other_description: string;
public line_from: number; public line_from: number;

View File

@ -10,6 +10,8 @@
<mat-form-field class="wide-form"> <mat-form-field class="wide-form">
<textarea matInput placeholder='Change recommendation Text' formControlName='text'></textarea> <textarea matInput placeholder='Change recommendation Text' formControlName='text'></textarea>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="public">{{ 'Public' | translate }}</mat-checkbox>
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>

View File

@ -24,7 +24,7 @@ export interface MotionChangeRecommendationComponentData {
* editChangeRecommendation: false, * editChangeRecommendation: false,
* newChangeRecommendation: true, * newChangeRecommendation: true,
* lineRange: lineRange, * lineRange: lineRange,
* motion: this.motion, * changeReco: this.changeRecommendation,
* }; * };
* this.dialogService.open(MotionChangeRecommendationComponent, { * this.dialogService.open(MotionChangeRecommendationComponent, {
* height: '400px', * height: '400px',
@ -104,14 +104,16 @@ export class MotionChangeRecommendationComponent {
public createForm(): void { public createForm(): void {
this.contentForm = this.formBuilder.group({ this.contentForm = this.formBuilder.group({
text: [this.changeReco.text, Validators.required], text: [this.changeReco.text, Validators.required],
diffType: [this.changeReco.type, Validators.required] diffType: [this.changeReco.type, Validators.required],
public: [!this.changeReco.internal]
}); });
} }
public saveChangeRecommendation(): void { public saveChangeRecommendation(): void {
this.changeReco.updateChangeReco( this.changeReco.updateChangeReco(
this.contentForm.controls.diffType.value, this.contentForm.controls.diffType.value,
this.contentForm.controls.text.value this.contentForm.controls.text.value,
!this.contentForm.controls.public.value
); );
if (this.newReco) { if (this.newReco) {

View File

@ -111,6 +111,10 @@
<span translate>Reject</span> <span translate>Reject</span>
<mat-icon *ngIf="change.isRejected()" class="active-indicator">done</mat-icon> <mat-icon *ngIf="change.isRejected()" class="active-indicator">done</mat-icon>
</button> </button>
<button type="button" mat-menu-item (click)="setInternal(change, !change.internal)">
<mat-icon>{{ change.internal ? "check_box_outline_blank" : "check_box" }}</mat-icon>
<span translate>Public</span>
</button>
<button type="button" mat-menu-item (click)="deleteChangeRecommendation(change, $event)"> <button type="button" mat-menu-item (click)="deleteChangeRecommendation(change, $event)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>

View File

@ -172,13 +172,23 @@ export class MotionDetailDiffComponent implements AfterViewInit {
*/ */
public setAcceptanceValue(change: ViewChangeReco, value: string): void { public setAcceptanceValue(change: ViewChangeReco, value: string): void {
if (value === 'accepted') { if (value === 'accepted') {
this.recoRepo.setAccepted(change).subscribe(() => {}); // Subscribe to trigger HTTP request this.recoRepo.setAccepted(change).subscribe(); // Subscribe to trigger HTTP request
} }
if (value === 'rejected') { if (value === 'rejected') {
this.recoRepo.setRejected(change).subscribe(() => {}); // Subscribe to trigger HTTP request this.recoRepo.setRejected(change).subscribe(); // Subscribe to trigger HTTP request
} }
} }
/**
* Sets if a change recommendation is internal or not
*
* @param {ViewChangeReco} change
* @param {boolean} internal
*/
public setInternal(change: ViewChangeReco, internal: boolean): void {
this.recoRepo.setInternal(change, internal).subscribe(); // Subscribe to trigger HTTP request
}
/** /**
* Deletes a change recommendation. * Deletes a change recommendation.
* The template has to make sure only to pass change recommendations to this method. * The template has to make sure only to pass change recommendations to this method.
@ -187,7 +197,7 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {MouseEvent} $event * @param {MouseEvent} $event
*/ */
public deleteChangeRecommendation(reco: ViewChangeReco, $event: MouseEvent): void { public deleteChangeRecommendation(reco: ViewChangeReco, $event: MouseEvent): void {
this.recoRepo.delete(reco).subscribe(() => {}); // Subscribe to trigger HTTP request this.recoRepo.delete(reco).subscribe(); // Subscribe to trigger HTTP request
$event.stopPropagation(); $event.stopPropagation();
$event.preventDefault(); $event.preventDefault();
} }

View File

@ -36,16 +36,21 @@ export class ViewChangeReco extends BaseViewModel implements ViewUnifiedChange {
// @TODO Is there any need for this function? // @TODO Is there any need for this function?
} }
public updateChangeReco(type: number, text: string): void { public updateChangeReco(type: number, text: string, internal: boolean): void {
// @TODO HTML sanitazion // @TODO HTML sanitazion
this._changeReco.type = type; this._changeReco.type = type;
this._changeReco.text = text; this._changeReco.text = text;
this._changeReco.internal = internal;
} }
public get rejected(): boolean { public get rejected(): boolean {
return this._changeReco ? this._changeReco.rejected : null; return this._changeReco ? this._changeReco.rejected : null;
} }
public get internal(): boolean {
return this._changeReco ? this._changeReco.internal : null;
}
public get type(): number { public get type(): number {
return this._changeReco ? this._changeReco.type : ModificationType.TYPE_REPLACEMENT; return this._changeReco ? this._changeReco.type : ModificationType.TYPE_REPLACEMENT;
} }

View File

@ -133,4 +133,18 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
}); });
return this.dataSend.updateModel(changeReco, HTTPMethod.PATCH) as Observable<MotionChangeReco>; return this.dataSend.updateModel(changeReco, HTTPMethod.PATCH) as Observable<MotionChangeReco>;
} }
/**
* Sets if a change recommendation is internal (for the administrators) or not.
*
* @param {ViewChangeReco} change
* @param {boolean} internal
*/
public setInternal(change: ViewChangeReco, internal: boolean): Observable<MotionChangeReco> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
internal: internal
});
return this.dataSend.updateModel(changeReco, HTTPMethod.PATCH) as Observable<MotionChangeReco>;
}
} }

View File

@ -88,6 +88,27 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
return MotionChangeRecommendationSerializer return MotionChangeRecommendationSerializer
def get_restricted_data(
self,
full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
"""
Removes change recommendations if they are internal and the user has
not the can_manage permission. To see change recommendation the user needs
the can_see permission.
"""
# Parse data.
if has_perm(user, 'motions.can_see'):
has_manage_perms = has_perm(user, 'motion.can_manage')
data = []
for full in full_data:
if not full['internal'] or has_manage_perms:
data.append(full)
else:
data = []
return data
class MotionCommentSectionAccessPermissions(BaseAccessPermissions): class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
""" """

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1 on 2018-10-20 18:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('motions', '0013_motion_sorting_and_statute'),
]
operations = [
migrations.AddField(
model_name='motionchangerecommendation',
name='internal',
field=models.BooleanField(default=True),
),
]

View File

@ -738,6 +738,9 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
rejected = models.BooleanField(default=False) rejected = models.BooleanField(default=False)
"""If true, this change recommendation has been rejected""" """If true, this change recommendation has been rejected"""
internal = models.BooleanField(default=True)
"""If true, this change recommendation can not be seen by regular users"""
type = models.PositiveIntegerField(default=0) type = models.PositiveIntegerField(default=0)
"""Replacement (0), Insertion (1), Deletion (2), Other (3)""" """Replacement (0), Insertion (1), Deletion (2), Other (3)"""

View File

@ -286,6 +286,7 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
'id', 'id',
'motion', 'motion',
'rejected', 'rejected',
'internal',
'type', 'type',
'other_description', 'other_description',
'line_from', 'line_from',

View File

@ -12,6 +12,7 @@ from openslides.motions.models import (
Category, Category,
Motion, Motion,
MotionBlock, MotionBlock,
MotionChangeRecommendation,
MotionComment, MotionComment,
MotionCommentSection, MotionCommentSection,
MotionLog, MotionLog,
@ -1235,6 +1236,57 @@ class TestMotionCommentSection(TestCase):
self.assertEqual(MotionCommentSection.objects.count(), 1) self.assertEqual(MotionCommentSection.objects.count(), 1)
class RetrieveMotionChangeRecommendation(TestCase):
"""
Tests retrieving motion change recommendations.
"""
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
motion = Motion(
title='test_title_3kd)K23,c9239mdj2wcG',
text='test_text_f8FLP,gvprC;wovVEwlQ')
motion.save()
self.public_cr = MotionChangeRecommendation(
motion=motion,
internal=False,
line_from=1,
line_to=1)
self.public_cr.save()
self.internal_cr = MotionChangeRecommendation(
motion=motion,
internal=True,
line_from=2,
line_to=2)
self.internal_cr.save()
def test_simple(self):
"""
Test retrieving all change recommendations.
"""
response = self.client.get(reverse('motionchangerecommendation-list'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)
def test_non_admin(self):
"""
Test retrieving of all change recommendations that are public, if the user
has no manage perms.
"""
self.admin = get_user_model().objects.get(username='admin')
self.admin.groups.add(GROUP_DELEGATE_PK)
self.admin.groups.remove(GROUP_ADMIN_PK)
inform_changed_data(self.admin)
response = self.client.get(reverse('motionchangerecommendation-list'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['id'], self.public_cr.id)
class CreateMotionChangeRecommendation(TestCase): class CreateMotionChangeRecommendation(TestCase):
""" """
Tests motion change recommendation creation. Tests motion change recommendation creation.