Merge pull request #3929 from FinnStutzenstein/motionComment
motion comments in the motion detail view
This commit is contained in:
commit
a098e5c5c9
@ -57,6 +57,10 @@ export class OperatorService extends OpenSlidesComponent {
|
|||||||
this.updatePermissions();
|
this.updatePermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isAnonymous(): boolean {
|
||||||
|
return !this.user || this.user.id === 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save, if quests are enabled.
|
* Save, if quests are enabled.
|
||||||
*/
|
*/
|
||||||
@ -132,6 +136,29 @@ export class OperatorService extends OpenSlidesComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true, if the operator is in at least one group or he is in the admin group.
|
||||||
|
* @param groups The groups to check
|
||||||
|
*/
|
||||||
|
public isInGroup(...groups: Group[]): boolean {
|
||||||
|
return this.isInGroupIds(...groups.map(group => group.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true, if the operator is in at least one group or he is in the admin group.
|
||||||
|
* @param groups The group ids to check
|
||||||
|
*/
|
||||||
|
public isInGroupIds(...groupIds: number[]): boolean {
|
||||||
|
if (!this.user) {
|
||||||
|
return groupIds.includes(1); // any anonymous is in the default group.
|
||||||
|
}
|
||||||
|
if (this.user.groups_id.includes(2)) {
|
||||||
|
// An admin has all perms and is technically in every group.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return groupIds.some(id => this.user.groups_id.includes(id));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the operators permissions and publish the operator afterwards.
|
* Update the operators permissions and publish the operator afterwards.
|
||||||
*/
|
*/
|
||||||
|
@ -1,13 +1,57 @@
|
|||||||
import { BaseModel } from '../base/base-model';
|
import { BaseModel } from '../base/base-model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content every personal note has.
|
||||||
|
*/
|
||||||
|
export interface PersonalNoteContent {
|
||||||
|
/**
|
||||||
|
* Users can star content to mark as favorite.
|
||||||
|
*/
|
||||||
|
star: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users can save their notes.
|
||||||
|
*/
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All notes are assigned to their object (given by collection string and id)
|
||||||
|
*/
|
||||||
|
export interface PersonalNotesFormat {
|
||||||
|
[collectionString: string]: {
|
||||||
|
[id: number]: PersonalNoteContent;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base personal note object.
|
||||||
|
*/
|
||||||
|
export interface PersonalNoteObject {
|
||||||
|
/**
|
||||||
|
* Every personal note object has an id.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user for the object.
|
||||||
|
*/
|
||||||
|
user_id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual notes arranged in a specific format.
|
||||||
|
*/
|
||||||
|
notes: PersonalNotesFormat;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of users personal note.
|
* Representation of users personal note.
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
export class PersonalNote extends BaseModel<PersonalNote> {
|
export class PersonalNote extends BaseModel<PersonalNote> implements PersonalNoteObject {
|
||||||
public id: number;
|
public id: number;
|
||||||
public user_id: number;
|
public user_id: number;
|
||||||
public notes: Object;
|
public notes: PersonalNotesFormat;
|
||||||
|
|
||||||
public constructor(input: any) {
|
public constructor(input: any) {
|
||||||
super('users/personal-note', input);
|
super('users/personal-note', input);
|
||||||
|
@ -108,14 +108,14 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
|
|||||||
/**
|
/**
|
||||||
* helper function to return one viewModel
|
* helper function to return one viewModel
|
||||||
*/
|
*/
|
||||||
protected getViewModel(id: number): V {
|
public getViewModel(id: number): V {
|
||||||
return this.viewModelStore[id];
|
return this.viewModelStore[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* helper function to return the viewModel as array
|
* helper function to return the viewModel as array
|
||||||
*/
|
*/
|
||||||
protected getViewModelList(): V[] {
|
public getViewModelList(): V[] {
|
||||||
return Object.values(this.viewModelStore);
|
return Object.values(this.viewModelStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<ng-container *ngIf="vp.isMobile ; then mobileView; else desktopView"></ng-container>
|
||||||
|
|
||||||
|
<ng-template #title><ng-content select=".meta-text-block-title"></ng-content></ng-template>
|
||||||
|
<ng-template #content><ng-content select=".meta-text-block-content"></ng-content></ng-template>
|
||||||
|
<ng-template #actionRow><ng-content select=".meta-text-block-action-row"></ng-content></ng-template>
|
||||||
|
|
||||||
|
<ng-template #mobileView>
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
<mat-icon>{{ icon }}</mat-icon>
|
||||||
|
<ng-container *ngTemplateOutlet="title"></ng-container>
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
|
||||||
|
<mat-action-row *ngIf="showActionRow">
|
||||||
|
<ng-container *ngTemplateOutlet="actionRow"></ng-container>
|
||||||
|
</mat-action-row>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #desktopView>
|
||||||
|
<mat-card class="meta-text-block">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>
|
||||||
|
<div class="title-container">
|
||||||
|
<div>
|
||||||
|
<ng-container *ngTemplateOutlet="title"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ng-container *ngTemplateOutlet="actionRow"></ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</ng-template>
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
mat-panel-title {
|
||||||
|
mat-icon {
|
||||||
|
margin-right: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-text-block {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 20px;
|
||||||
|
margin-right: 0;
|
||||||
|
min-width: 10hv;
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
mat-card-header {
|
||||||
|
display: inherit;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #eee;
|
||||||
|
|
||||||
|
mat-card-title {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
:host ::ng-deep button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-card-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MetaTextBlockComponent } from './meta-text-block.component';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('MetaTextBlockComponent', () => {
|
||||||
|
let component: MetaTextBlockComponent;
|
||||||
|
let fixture: ComponentFixture<MetaTextBlockComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [MetaTextBlockComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MetaTextBlockComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { BaseComponent } from '../../../../base.component';
|
||||||
|
import { ViewportService } from '../../../../core/services/viewport.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for the motion comments view
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-meta-text-block',
|
||||||
|
templateUrl: './meta-text-block.component.html',
|
||||||
|
styleUrls: ['./meta-text-block.component.scss']
|
||||||
|
})
|
||||||
|
export class MetaTextBlockComponent extends BaseComponent {
|
||||||
|
@Input()
|
||||||
|
public showActionRow: boolean;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public icon: string;
|
||||||
|
|
||||||
|
public constructor(public vp: ViewportService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<os-meta-text-block *ngFor="let section of sections" [showActionRow]="canEditSection(section)" icon="comment">
|
||||||
|
<ng-container class="meta-text-block-title">
|
||||||
|
<span>{{ section.getTitle() }}</span>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container class="meta-text-block-content">
|
||||||
|
<ng-container *ngIf="!isCommentEdited(section)">
|
||||||
|
<div *ngIf="comments[section.id]" [innerHTML]="comments[section.id].comment"></div>
|
||||||
|
<div class="no-content" *ngIf="!comments[section.id] || !comments[section.id].comment" translate>
|
||||||
|
No comment
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<form [formGroup]="commentForms[section.id]" *ngIf="isCommentEdited(section)">
|
||||||
|
<mat-form-field>
|
||||||
|
<textarea formControlName="comment" matInput placeholder="{{'Comment' | translate}}"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container class="meta-text-block-action-row">
|
||||||
|
<button mat-icon-button *ngIf="!isCommentEdited(section)" (click)="editComment(section)">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button *ngIf="isCommentEdited(section)" (click)="saveComment(section)">
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button *ngIf="isCommentEdited(section)" (click)="cancelEditing(section)">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</os-meta-text-block>
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MotionCommentsComponent } from './motion-comments.component';
|
||||||
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
import { MetaTextBlockComponent } from '../meta-text-block/meta-text-block.component';
|
||||||
|
|
||||||
|
describe('MotionCommentsComponent', () => {
|
||||||
|
let component: MotionCommentsComponent;
|
||||||
|
let fixture: ComponentFixture<MotionCommentsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [MetaTextBlockComponent, MotionCommentsComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MotionCommentsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,145 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { BaseComponent } from '../../../../base.component';
|
||||||
|
import { ViewportService } from '../../../../core/services/viewport.service';
|
||||||
|
import { MotionCommentSectionRepositoryService } from '../../services/motion-comment-section-repository.service';
|
||||||
|
import { ViewMotionCommentSection } from '../../models/view-motion-comment-section';
|
||||||
|
import { OperatorService } from '../../../../core/services/operator.service';
|
||||||
|
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||||
|
import { MotionComment } from '../../../../shared/models/motions/motion-comment';
|
||||||
|
import { ViewMotion } from '../../models/view-motion';
|
||||||
|
import { HttpService } from '../../../../core/services/http.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for the motion comments view
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-motion-comments',
|
||||||
|
templateUrl: './motion-comments.component.html',
|
||||||
|
styleUrls: ['./motion-comments.component.scss']
|
||||||
|
})
|
||||||
|
export class MotionCommentsComponent extends BaseComponent {
|
||||||
|
/**
|
||||||
|
* An array of all sections the operator can see.
|
||||||
|
*/
|
||||||
|
public sections: ViewMotionCommentSection[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of forms for one comment mapped to the section id.
|
||||||
|
*/
|
||||||
|
public commentForms: { [id: number]: FormGroup } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object holds all comments for each section for the given motion.
|
||||||
|
*/
|
||||||
|
public comments: { [id: number]: MotionComment } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The motion, which these comments belong to.
|
||||||
|
*/
|
||||||
|
private _motion: ViewMotion;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public set motion(motion: ViewMotion) {
|
||||||
|
this._motion = motion;
|
||||||
|
this.updateComments();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get motion(): ViewMotion {
|
||||||
|
return this._motion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for changes in sections and the operator. If one of them changes, the sections are reloaded
|
||||||
|
* and the comments updated.
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private commentRepo: MotionCommentSectionRepositoryService,
|
||||||
|
private http: HttpService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
public vp: ViewportService,
|
||||||
|
private operator: OperatorService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.commentRepo.getViewModelListObservable().subscribe(sections => this.setSections(sections));
|
||||||
|
this.operator.getObservable().subscribe(() => this.setSections(this.commentRepo.getViewModelList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets the `sections` member with sections, if the operator has reading permissions.
|
||||||
|
* @param allSections A list of all sections available
|
||||||
|
*/
|
||||||
|
private setSections(allSections: ViewMotionCommentSection[]): void {
|
||||||
|
this.sections = allSections.filter(section => this.operator.isInGroupIds(...section.read_groups_id));
|
||||||
|
this.updateComments();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the operator has write permissions for the given section, so he can edit the comment.
|
||||||
|
* @param section The section to judge about
|
||||||
|
*/
|
||||||
|
public canEditSection(section: ViewMotionCommentSection): boolean {
|
||||||
|
return this.operator.isInGroupIds(...section.write_groups_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the comments. Comments are saved in the `comments` object associated with their section id.
|
||||||
|
*/
|
||||||
|
private updateComments(): void {
|
||||||
|
this.comments = {};
|
||||||
|
if (!this.motion || !this.sections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sections.forEach(section => {
|
||||||
|
this.comments[section.id] = this.motion.getCommentForSection(section);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts the comment into edit mode.
|
||||||
|
* @param section The section for the comment.
|
||||||
|
*/
|
||||||
|
public editComment(section: ViewMotionCommentSection): void {
|
||||||
|
const comment = this.comments[section.id];
|
||||||
|
const form = this.formBuilder.group({
|
||||||
|
comment: [comment ? comment.comment : '']
|
||||||
|
});
|
||||||
|
this.commentForms[section.id] = form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the comment. Makes a request to the server.
|
||||||
|
* @param section The section for the comment to save
|
||||||
|
*/
|
||||||
|
public async saveComment(section: ViewMotionCommentSection): Promise<void> {
|
||||||
|
const commentText = this.commentForms[section.id].get('comment').value;
|
||||||
|
try {
|
||||||
|
await this.http
|
||||||
|
.post(`rest/motions/motion/${this.motion.id}/manage_comments/`, {
|
||||||
|
section_id: section.id,
|
||||||
|
comment: commentText
|
||||||
|
});
|
||||||
|
this.cancelEditing(section);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
// TODO: Errorhandling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancles the editing for a comment.
|
||||||
|
* @param section The section for the comment
|
||||||
|
*/
|
||||||
|
public cancelEditing(section: ViewMotionCommentSection): void {
|
||||||
|
delete this.commentForms[section.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true, if the comment is edited.
|
||||||
|
* @param section The section for the comment.
|
||||||
|
*/
|
||||||
|
public isCommentEdited(section: ViewMotionCommentSection): boolean {
|
||||||
|
return Object.keys(this.commentForms).includes('' + section.id);
|
||||||
|
}
|
||||||
|
}
|
@ -83,16 +83,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<!-- Personal Note -->
|
<os-motion-comments [motion]="motion"></os-motion-comments>
|
||||||
<mat-expansion-panel>
|
<os-personal-note [motion]="motion"></os-personal-note>
|
||||||
<mat-expansion-panel-header>
|
|
||||||
<mat-panel-title>
|
|
||||||
<mat-icon icon>speaker_notes</mat-icon>
|
|
||||||
<span translate>Personal note</span>
|
|
||||||
</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
TEST
|
|
||||||
</mat-expansion-panel>
|
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<mat-expansion-panel #contentPanel [expanded]='true'>
|
<mat-expansion-panel #contentPanel [expanded]='true'>
|
||||||
@ -119,24 +111,8 @@
|
|||||||
<ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Personal Note -->
|
<os-motion-comments [motion]="motion"></os-motion-comments>
|
||||||
<div class="personal-note">
|
<os-personal-note [motion]="motion"></os-personal-note>
|
||||||
<mat-card>
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title>
|
|
||||||
<span translate>Personal Note</span>
|
|
||||||
<div class="title-right">
|
|
||||||
<mat-icon>add</mat-icon>
|
|
||||||
<mat-icon>more_vert</mat-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
Hier könnte ihre Werbung stehen. 1 2 3 4 5 6 Hier könnte ihre Werbung stehen. 1 2 3 4 5 6
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop-right ">
|
<div class="desktop-right ">
|
||||||
|
@ -47,12 +47,6 @@ span {
|
|||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-panel-title {
|
|
||||||
mat-icon {
|
|
||||||
margin-right: 35px; //on line with text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-info-block {
|
.meta-info-block {
|
||||||
form {
|
form {
|
||||||
div + div {
|
div + div {
|
||||||
@ -153,42 +147,6 @@ mat-panel-title {
|
|||||||
.meta-info-desktop {
|
.meta-info-desktop {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.personal-note {
|
|
||||||
mat-card {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 20px;
|
|
||||||
min-width: 10hv;
|
|
||||||
min-width: 200px;
|
|
||||||
|
|
||||||
.mat-card-header-text {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-header {
|
|
||||||
display: inherit;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 0;
|
|
||||||
background-color: #eee;
|
|
||||||
|
|
||||||
.title-right {
|
|
||||||
float: right;
|
|
||||||
mat-icon {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-title {
|
|
||||||
font-weight: bold;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-content {
|
|
||||||
padding: 30px 15px 15px 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-right {
|
.desktop-right {
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
<os-meta-text-block showActionRow="true" icon="speaker_notes">
|
||||||
|
<ng-container class="meta-text-block-title">
|
||||||
|
<span translate>Personal note</span>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container class="meta-text-block-content">
|
||||||
|
<ng-container *ngIf="!isEditMode">
|
||||||
|
<div *ngIf="personalNote" [innerHTML]="personalNote.note"></div>
|
||||||
|
<div class="no-content" *ngIf="!personalNote" translate>
|
||||||
|
No personal note
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<form [formGroup]="personalNoteForm" *ngIf="isEditMode">
|
||||||
|
<mat-form-field>
|
||||||
|
<textarea formControlName="note" matInput placeholder="{{'Personal note' | translate}}"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container class="meta-text-block-action-row">
|
||||||
|
<button mat-icon-button *ngIf="!isEditMode" (click)="editPersonalNote()">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button *ngIf="isEditMode" (click)="savePersonalNote()">
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button *ngIf="isEditMode" (click)="isEditMode = false">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</os-meta-text-block>
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PersonalNoteComponent } from './personal-note.component';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
import { MetaTextBlockComponent } from '../meta-text-block/meta-text-block.component';
|
||||||
|
|
||||||
|
describe('PersonalNoteComponent', () => {
|
||||||
|
let component: PersonalNoteComponent;
|
||||||
|
let fixture: ComponentFixture<PersonalNoteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [MetaTextBlockComponent, PersonalNoteComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PersonalNoteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,115 @@
|
|||||||
|
import { Component, Input, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
|
import { BaseComponent } from '../../../../base.component';
|
||||||
|
import { ViewMotion } from '../../models/view-motion';
|
||||||
|
import { PersonalNoteService } from '../../services/personal-note.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import { PersonalNoteContent } from '../../../../shared/models/users/personal-note';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for the motion comments view
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-personal-note',
|
||||||
|
templateUrl: './personal-note.component.html',
|
||||||
|
styleUrls: ['./personal-note.component.scss']
|
||||||
|
})
|
||||||
|
export class PersonalNoteComponent extends BaseComponent implements OnDestroy {
|
||||||
|
/**
|
||||||
|
* The motion, which the personal note belong to.
|
||||||
|
*/
|
||||||
|
private _motion: ViewMotion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the motion. If the motion updates (changes, and so on), the subscription
|
||||||
|
* for the personal note will be established.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set motion(motion: ViewMotion) {
|
||||||
|
this._motion = motion;
|
||||||
|
if (this.personalNoteSubscription) {
|
||||||
|
this.personalNoteSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
if (motion && motion.motion) {
|
||||||
|
this.personalNoteSubscription = this.personalNoteService
|
||||||
|
.getPersonalNoteObserver(motion.motion)
|
||||||
|
.subscribe(pn => {
|
||||||
|
this.personalNote = pn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get motion(): ViewMotion {
|
||||||
|
return this._motion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The edit form for the note
|
||||||
|
*/
|
||||||
|
public personalNoteForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves, if the users edits the note.
|
||||||
|
*/
|
||||||
|
public isEditMode = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The personal note.
|
||||||
|
*/
|
||||||
|
public personalNote: PersonalNoteContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subscription for the personal note.
|
||||||
|
*/
|
||||||
|
private personalNoteSubscription: Subscription;
|
||||||
|
|
||||||
|
public constructor(private personalNoteService: PersonalNoteService, formBuilder: FormBuilder) {
|
||||||
|
super();
|
||||||
|
this.personalNoteForm = formBuilder.group({
|
||||||
|
note: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the form.
|
||||||
|
*/
|
||||||
|
public editPersonalNote(): void {
|
||||||
|
this.personalNoteForm.reset();
|
||||||
|
this.personalNoteForm.patchValue({
|
||||||
|
note: this.personalNote ? this.personalNote.note : ''
|
||||||
|
});
|
||||||
|
this.isEditMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the personal note. If it does not exists, it will be created.
|
||||||
|
*/
|
||||||
|
public async savePersonalNote(): Promise<void> {
|
||||||
|
let content: PersonalNoteContent;
|
||||||
|
if (this.personalNote) {
|
||||||
|
content = Object.assign({}, this.personalNote);
|
||||||
|
content.note = this.personalNoteForm.get('note').value;
|
||||||
|
} else {
|
||||||
|
content = {
|
||||||
|
note: this.personalNoteForm.get('note').value,
|
||||||
|
star: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.personalNoteService.savePersonalNote(this.motion.motion, content);
|
||||||
|
this.isEditMode = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the subscription if this component isn't needed anymore.
|
||||||
|
*/
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
if (this.personalNoteSubscription) {
|
||||||
|
this.personalNoteSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -105,7 +105,7 @@
|
|||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
<mat-card *ngIf="statuteParagraphs.length === 0">
|
<mat-card *ngIf="statuteParagraphs.length === 0">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="noContent" translate>No statute paragraphs yet...</div>
|
<div class="no-content" translate>No statute paragraphs yet...</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
|
@ -10,8 +10,3 @@
|
|||||||
mat-card {
|
mat-card {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noContent {
|
|
||||||
text-align: center;
|
|
||||||
color: gray; /* TODO: remove this and replace with theme */
|
|
||||||
}
|
|
||||||
|
@ -73,9 +73,7 @@ export class ViewMotionCommentSection extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement updating of groups
|
// TODO: Implement updating of groups
|
||||||
public updateGroup(group: Group): void {
|
public updateGroup(group: Group): void {}
|
||||||
console.log(this._section, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplicate this motion into a copy of itself
|
* Duplicate this motion into a copy of itself
|
||||||
|
@ -5,6 +5,8 @@ import { Workflow } from '../../../shared/models/motions/workflow';
|
|||||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||||
import { BaseModel } from '../../../shared/models/base/base-model';
|
import { BaseModel } from '../../../shared/models/base/base-model';
|
||||||
import { BaseViewModel } from '../../base/base-view-model';
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
import { ViewMotionCommentSection } from './view-motion-comment-section';
|
||||||
|
import { MotionComment } from '../../../shared/models/motions/motion-comment';
|
||||||
|
|
||||||
export enum LineNumberingMode {
|
export enum LineNumberingMode {
|
||||||
None,
|
None,
|
||||||
@ -146,29 +148,13 @@ export class ViewMotion extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public set supporters(users: User[]) {
|
public set supporters(users: User[]) {
|
||||||
const userIDArr: number[] = [];
|
|
||||||
users.forEach(user => {
|
|
||||||
userIDArr.push(user.id);
|
|
||||||
});
|
|
||||||
this._supporters = users;
|
this._supporters = users;
|
||||||
this._motion.supporters_id = userIDArr;
|
this._motion.supporters_id = users.map(user => user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public set submitters(users: User[]) {
|
public set submitters(users: User[]) {
|
||||||
// For the newer backend with weight:
|
|
||||||
// const submitterArr: MotionSubmitter[] = []
|
|
||||||
// users.forEach(user => {
|
|
||||||
// const motionSub = new MotionSubmitter();
|
|
||||||
// submitterArr.push(motionSub);
|
|
||||||
// });
|
|
||||||
// this._motion.submitters = submitterArr;
|
|
||||||
this._submitters = users;
|
this._submitters = users;
|
||||||
const submitterIDArr: number[] = [];
|
this._motion.submitters_id = users.map(user => user.id);
|
||||||
// for the older backend:
|
|
||||||
users.forEach(user => {
|
|
||||||
submitterIDArr.push(user.id);
|
|
||||||
});
|
|
||||||
this._motion.submitters_id = submitterIDArr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -204,6 +190,17 @@ export class ViewMotion extends BaseViewModel {
|
|||||||
return this.title;
|
return this.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the motion comment for the given section. Null, if no comment exist.
|
||||||
|
* @param section The section to search the comment for.
|
||||||
|
*/
|
||||||
|
public getCommentForSection(section: ViewMotionCommentSection): MotionComment {
|
||||||
|
if (!this.motion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.motion.comments.find(comment => comment.section_id === section.id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the local objects if required
|
* Updates the local objects if required
|
||||||
* @param update
|
* @param update
|
||||||
|
@ -11,6 +11,9 @@ import { StatuteParagraphListComponent } from './components/statute-paragraph-li
|
|||||||
import { MotionChangeRecommendationComponent } from './components/motion-change-recommendation/motion-change-recommendation.component';
|
import { MotionChangeRecommendationComponent } from './components/motion-change-recommendation/motion-change-recommendation.component';
|
||||||
import { MotionDetailOriginalChangeRecommendationsComponent } from './components/motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
import { MotionDetailOriginalChangeRecommendationsComponent } from './components/motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
||||||
import { MotionDetailDiffComponent } from './components/motion-detail-diff/motion-detail-diff.component';
|
import { MotionDetailDiffComponent } from './components/motion-detail-diff/motion-detail-diff.component';
|
||||||
|
import { MotionCommentsComponent } from './components/motion-comments/motion-comments.component';
|
||||||
|
import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-block.component';
|
||||||
|
import { PersonalNoteComponent } from './components/personal-note/personal-note.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
||||||
@ -21,10 +24,19 @@ import { MotionDetailDiffComponent } from './components/motion-detail-diff/motio
|
|||||||
MotionCommentSectionListComponent,
|
MotionCommentSectionListComponent,
|
||||||
StatuteParagraphListComponent,
|
StatuteParagraphListComponent,
|
||||||
MotionChangeRecommendationComponent,
|
MotionChangeRecommendationComponent,
|
||||||
MotionCommentSectionListComponent,
|
|
||||||
MotionDetailOriginalChangeRecommendationsComponent,
|
MotionDetailOriginalChangeRecommendationsComponent,
|
||||||
MotionDetailDiffComponent
|
MotionDetailDiffComponent,
|
||||||
|
MotionCommentsComponent,
|
||||||
|
MetaTextBlockComponent,
|
||||||
|
PersonalNoteComponent
|
||||||
],
|
],
|
||||||
entryComponents: [MotionChangeRecommendationComponent]
|
entryComponents: [
|
||||||
|
MotionChangeRecommendationComponent,
|
||||||
|
StatuteParagraphListComponent,
|
||||||
|
MotionCommentsComponent,
|
||||||
|
MotionCommentSectionListComponent,
|
||||||
|
MetaTextBlockComponent,
|
||||||
|
PersonalNoteComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class MotionsModule {}
|
export class MotionsModule {}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PersonalNoteService } from './personal-note.service';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('PersonalNoteService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
providers: [PersonalNoteService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([PersonalNoteService], (service: PersonalNoteService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
130
client/src/app/site/motions/services/personal-note.service.ts
Normal file
130
client/src/app/site/motions/services/personal-note.service.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||||
|
import { OperatorService } from '../../../core/services/operator.service';
|
||||||
|
import { PersonalNote, PersonalNoteObject, PersonalNoteContent } from '../../../shared/models/users/personal-note';
|
||||||
|
import { BaseModel } from '../../../shared/models/base/base-model';
|
||||||
|
import { HttpService } from '../../../core/services/http.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All subjects are organized by the collection string and id of the model.
|
||||||
|
*/
|
||||||
|
interface PersonalNoteSubjects {
|
||||||
|
[collectionString: string]: {
|
||||||
|
[id: number]: BehaviorSubject<PersonalNoteContent>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles personal notes.
|
||||||
|
*
|
||||||
|
* Get updated by subscribing to `getPersonalNoteObserver`. Save personal notes by calling
|
||||||
|
* `savePersonalNote`.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PersonalNoteService {
|
||||||
|
/**
|
||||||
|
* The personal note object for the operator
|
||||||
|
*/
|
||||||
|
private personalNoteObject: PersonalNoteObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All subjects for all observers.
|
||||||
|
*/
|
||||||
|
private subjects: PersonalNoteSubjects = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for changes in the personal note model.
|
||||||
|
*/
|
||||||
|
public constructor(private operator: OperatorService, private DS: DataStoreService, private http: HttpService) {
|
||||||
|
operator.getObservable().subscribe(() => this.updatePersonalNoteObject());
|
||||||
|
this.DS.changeObservable.subscribe(model => {
|
||||||
|
if (model instanceof PersonalNote) {
|
||||||
|
this.updatePersonalNoteObject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the personal note object and notifies the subscribers.
|
||||||
|
*/
|
||||||
|
private updatePersonalNoteObject(): void {
|
||||||
|
if (this.operator.isAnonymous) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the note for the operator.
|
||||||
|
const operatorId = this.operator.user.id;
|
||||||
|
const objects = this.DS.filter(PersonalNote, pn => pn.user_id === operatorId);
|
||||||
|
this.personalNoteObject = objects.length === 0 ? null : objects[0];
|
||||||
|
|
||||||
|
this.updateSubscribers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all subscribers.
|
||||||
|
*/
|
||||||
|
private updateSubscribers(): void {
|
||||||
|
Object.keys(this.subjects).forEach(collectionString => {
|
||||||
|
Object.keys(this.subjects[collectionString]).forEach(id => {
|
||||||
|
this.subjects[collectionString][id].next(this.getPersonalNoteContent(collectionString, +id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the content from a note by the collection string and id.
|
||||||
|
*/
|
||||||
|
private getPersonalNoteContent(collectionString: string, id: number): PersonalNoteContent {
|
||||||
|
if (
|
||||||
|
!this.personalNoteObject ||
|
||||||
|
!this.personalNoteObject.notes ||
|
||||||
|
!this.personalNoteObject.notes[collectionString] ||
|
||||||
|
!this.personalNoteObject.notes[collectionString][id]
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.personalNoteObject.notes[collectionString][id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observalbe for a given BaseModel.
|
||||||
|
* @param model The model to observe the personal note from.
|
||||||
|
*/
|
||||||
|
public getPersonalNoteObserver(model: BaseModel): Observable<PersonalNoteContent> {
|
||||||
|
if (!this.subjects[model.collectionString]) {
|
||||||
|
this.subjects[model.collectionString] = {};
|
||||||
|
}
|
||||||
|
if (!this.subjects[model.collectionString][model.id]) {
|
||||||
|
const subject = new BehaviorSubject<PersonalNoteContent>(
|
||||||
|
this.getPersonalNoteContent(model.collectionString, model.id)
|
||||||
|
);
|
||||||
|
this.subjects[model.collectionString][model.id] = subject;
|
||||||
|
}
|
||||||
|
return this.subjects[model.collectionString][model.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the personal note for the given model.
|
||||||
|
* @param model The model the content belongs to
|
||||||
|
* @param content The new content.
|
||||||
|
*/
|
||||||
|
public async savePersonalNote(model: BaseModel, content: PersonalNoteContent): Promise<void> {
|
||||||
|
const pnObject: Partial<PersonalNoteObject> = this.personalNoteObject || {};
|
||||||
|
if (!pnObject.notes) {
|
||||||
|
pnObject.notes = {};
|
||||||
|
}
|
||||||
|
if (!pnObject.notes[model.collectionString]) {
|
||||||
|
pnObject.notes[model.collectionString] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
pnObject.notes[model.collectionString][model.id] = content;
|
||||||
|
if (!pnObject.id) {
|
||||||
|
await this.http.post('rest/users/personal-note/', pnObject);
|
||||||
|
} else {
|
||||||
|
await this.http.put(`rest/users/personal-note/${pnObject.id}/`, pnObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,11 @@ body {
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
.os-card {
|
.os-card {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
@ -300,7 +300,7 @@ class MotionViewSet(ModelViewSet):
|
|||||||
@detail_route(methods=['POST', 'DELETE'])
|
@detail_route(methods=['POST', 'DELETE'])
|
||||||
def manage_comments(self, request, pk=None):
|
def manage_comments(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
Create, update and delete motin comments.
|
Create, update and delete motion comments.
|
||||||
Send a post request with {'section_id': <id>, 'comment': '<comment>'} to create
|
Send a post request with {'section_id': <id>, 'comment': '<comment>'} to create
|
||||||
a new comment or update an existing comment.
|
a new comment or update an existing comment.
|
||||||
Send a delete request with just {'section_id': <id>} to delete the comment.
|
Send a delete request with just {'section_id': <id>} to delete the comment.
|
||||||
|
Loading…
Reference in New Issue
Block a user