2018-09-04 14:55:07 +02:00
|
|
|
import { Injectable } from '@angular/core';
|
2019-01-21 16:34:19 +01:00
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
2018-09-04 14:55:07 +02:00
|
|
|
|
2018-11-22 15:14:01 +01:00
|
|
|
import { Observable } from 'rxjs';
|
2018-11-04 11:11:48 +01:00
|
|
|
import { tap, map } from 'rxjs/operators';
|
2018-11-22 15:14:01 +01:00
|
|
|
|
2019-01-17 10:53:16 +01:00
|
|
|
import { BaseRepository } from '../../base/base-repository';
|
2018-09-04 14:55:07 +02:00
|
|
|
import { Category } from '../../../shared/models/motions/category';
|
2019-01-17 17:13:34 +01:00
|
|
|
import { ChangeRecoMode, ViewMotion } from '../models/view-motion';
|
2019-01-17 10:53:16 +01:00
|
|
|
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
|
|
|
import { CreateMotion } from '../models/create-motion';
|
|
|
|
import { DataSendService } from '../../../core/services/data-send.service';
|
2018-09-13 14:40:04 +02:00
|
|
|
import { DataStoreService } from '../../../core/services/data-store.service';
|
2018-11-04 11:11:48 +01:00
|
|
|
import { DiffLinesInParagraph, DiffService, LineRange, ModificationType } from './diff.service';
|
2018-11-05 17:43:44 +01:00
|
|
|
import { HttpService } from 'app/core/services/http.service';
|
2019-01-17 10:53:16 +01:00
|
|
|
import { Identifiable } from '../../../shared/models/base/identifiable';
|
2018-11-12 15:24:23 +01:00
|
|
|
import { Item } from 'app/shared/models/agenda/item';
|
2019-01-17 10:53:16 +01:00
|
|
|
import { LinenumberingService } from './linenumbering.service';
|
|
|
|
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
|
|
|
import { Motion } from '../../../shared/models/motions/motion';
|
|
|
|
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
|
|
|
import { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco';
|
|
|
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
2018-11-16 10:25:17 +01:00
|
|
|
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
2019-01-17 10:53:16 +01:00
|
|
|
import { PersonalNoteService } from './personal-note.service';
|
2018-11-22 15:14:01 +01:00
|
|
|
import { TreeService } from 'app/core/services/tree.service';
|
2019-01-17 10:53:16 +01:00
|
|
|
import { User } from '../../../shared/models/users/user';
|
|
|
|
import { ViewChangeReco } from '../models/view-change-reco';
|
2018-11-04 11:11:48 +01:00
|
|
|
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
2019-01-17 10:53:16 +01:00
|
|
|
import { ViewUnifiedChange } from '../models/view-unified-change';
|
|
|
|
import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
|
|
|
|
import { Workflow } from '../../../shared/models/motions/workflow';
|
|
|
|
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
2018-09-04 14:55:07 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Repository Services for motions (and potentially categories)
|
|
|
|
*
|
|
|
|
* The repository is meant to process domain objects (those found under
|
|
|
|
* shared/models), so components can display them and interact with them.
|
|
|
|
*
|
|
|
|
* Rather than manipulating models directly, the repository is meant to
|
|
|
|
* inform the {@link DataSendService} about changes which will send
|
|
|
|
* them to the Server.
|
|
|
|
*/
|
|
|
|
@Injectable({
|
|
|
|
providedIn: 'root'
|
|
|
|
})
|
2018-09-10 15:53:11 +02:00
|
|
|
export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion> {
|
2018-09-04 14:55:07 +02:00
|
|
|
/**
|
|
|
|
* Creates a MotionRepository
|
|
|
|
*
|
|
|
|
* Converts existing and incoming motions to ViewMotions
|
|
|
|
* Handles CRUD using an observer to the DataStore
|
2018-11-09 13:44:39 +01:00
|
|
|
*
|
|
|
|
* @param DS The DataStore
|
|
|
|
* @param mapperService Maps collection strings to classes
|
|
|
|
* @param dataSend sending changed objects
|
|
|
|
* @param httpService OpenSlides own Http service
|
|
|
|
* @param lineNumbering Line numbering for motion text
|
|
|
|
* @param diff Display changes in motion text as diff.
|
2019-01-17 10:53:16 +01:00
|
|
|
* @param personalNoteService service fo personal notes
|
2018-09-04 14:55:07 +02:00
|
|
|
*/
|
2018-09-09 18:52:47 +02:00
|
|
|
public constructor(
|
|
|
|
DS: DataStoreService,
|
2018-11-02 08:34:33 +01:00
|
|
|
mapperService: CollectionStringModelMapperService,
|
2018-09-09 18:52:47 +02:00
|
|
|
private dataSend: DataSendService,
|
2018-11-05 17:43:44 +01:00
|
|
|
private httpService: HttpService,
|
2018-09-09 18:52:47 +02:00
|
|
|
private readonly lineNumbering: LinenumberingService,
|
2018-11-22 15:14:01 +01:00
|
|
|
private readonly diff: DiffService,
|
2019-01-17 10:53:16 +01:00
|
|
|
private treeService: TreeService,
|
2019-01-21 16:34:19 +01:00
|
|
|
private personalNoteService: PersonalNoteService,
|
|
|
|
private translate: TranslateService
|
2018-09-09 18:52:47 +02:00
|
|
|
) {
|
2018-12-10 17:54:48 +01:00
|
|
|
super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile]);
|
2018-09-04 14:55:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a motion to a ViewMotion and adds it to the store.
|
|
|
|
*
|
|
|
|
* Foreign references of the motion will be resolved (e.g submitters to users)
|
|
|
|
* Expandable to all (server side) changes that might occur on the motion object.
|
|
|
|
*
|
|
|
|
* @param motion blank motion domain object
|
|
|
|
*/
|
2018-09-10 15:53:11 +02:00
|
|
|
protected createViewModel(motion: Motion): ViewMotion {
|
2018-09-04 14:55:07 +02:00
|
|
|
const category = this.DS.get(Category, motion.category_id);
|
|
|
|
const submitters = this.DS.getMany(User, motion.submitterIds);
|
|
|
|
const supporters = this.DS.getMany(User, motion.supporters_id);
|
|
|
|
const workflow = this.DS.get(Workflow, motion.workflow_id);
|
2018-11-12 15:24:23 +01:00
|
|
|
const item = this.DS.get(Item, motion.agenda_item_id);
|
2018-12-06 12:28:05 +01:00
|
|
|
const block = this.DS.get(MotionBlock, motion.motion_block_id);
|
2018-12-10 17:54:48 +01:00
|
|
|
const attachments = this.DS.getMany(Mediafile, motion.attachments_id);
|
2018-09-04 14:55:07 +02:00
|
|
|
let state: WorkflowState = null;
|
|
|
|
if (workflow) {
|
|
|
|
state = workflow.getStateById(motion.state_id);
|
|
|
|
}
|
2019-01-17 17:13:34 +01:00
|
|
|
return new ViewMotion(motion, category, submitters, supporters, workflow, state, item, block, attachments);
|
2018-09-04 14:55:07 +02:00
|
|
|
}
|
|
|
|
|
2018-11-23 13:42:44 +01:00
|
|
|
/**
|
|
|
|
* Add custom hook into the observables. The motions get a virtual weight (a sequential number) for the
|
|
|
|
* call list order. One can just sort for this number instead of dealing with the sort parent id and weight.
|
|
|
|
*
|
|
|
|
* @override
|
|
|
|
*/
|
2018-11-22 15:14:01 +01:00
|
|
|
public getViewModelListObservable(): Observable<ViewMotion[]> {
|
|
|
|
return super.getViewModelListObservable().pipe(
|
|
|
|
tap(motions => {
|
|
|
|
const iterator = this.treeService.traverseItems(motions, 'weight', 'sort_parent_id');
|
|
|
|
let m: IteratorResult<ViewMotion>;
|
|
|
|
let virtualWeightCounter = 0;
|
|
|
|
while (!(m = iterator.next()).done) {
|
|
|
|
m.value.callListWeight = virtualWeightCounter++;
|
2019-01-17 10:53:16 +01:00
|
|
|
const motion = m.value;
|
|
|
|
this.personalNoteService
|
|
|
|
.getPersonalNoteObserver(motion.motion)
|
|
|
|
.subscribe(note => (motion.personalNote = note));
|
2018-11-22 15:14:01 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-09-04 14:55:07 +02:00
|
|
|
/**
|
2018-09-13 07:57:38 +02:00
|
|
|
* Creates a motion
|
|
|
|
* Creates a (real) motion with patched data and delegate it
|
|
|
|
* to the {@link DataSendService}
|
|
|
|
*
|
2018-11-23 13:59:14 +01:00
|
|
|
* @param update the form data containing the updated values
|
2018-09-13 07:57:38 +02:00
|
|
|
* @param viewMotion The View Motion. If not present, a new motion will be created
|
|
|
|
*/
|
2018-11-30 09:24:07 +01:00
|
|
|
public async create(motion: CreateMotion): Promise<Identifiable> {
|
2018-12-04 19:31:24 +01:00
|
|
|
// TODO how to handle category id and motion_block id in CreateMotion?
|
2018-10-26 11:19:05 +02:00
|
|
|
return await this.dataSend.createModel(motion);
|
2018-09-13 07:57:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* updates a motion
|
2018-09-04 14:55:07 +02:00
|
|
|
*
|
|
|
|
* Creates a (real) motion with patched data and delegate it
|
|
|
|
* to the {@link DataSendService}
|
|
|
|
*
|
2018-11-23 13:59:14 +01:00
|
|
|
* @param update the form data containing the updated values
|
2018-09-04 14:55:07 +02:00
|
|
|
* @param viewMotion The View Motion. If not present, a new motion will be created
|
|
|
|
*/
|
2018-10-26 11:19:05 +02:00
|
|
|
public async update(update: Partial<Motion>, viewMotion: ViewMotion): Promise<void> {
|
2018-09-20 12:25:37 +02:00
|
|
|
const motion = viewMotion.motion;
|
|
|
|
motion.patchValues(update);
|
2018-11-05 17:40:32 +01:00
|
|
|
return await this.dataSend.partialUpdateModel(motion);
|
2018-09-04 14:55:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deleting a motion.
|
|
|
|
*
|
|
|
|
* Extract the motion out of the motionView and delegate
|
|
|
|
* to {@link DataSendService}
|
|
|
|
* @param viewMotion
|
|
|
|
*/
|
2018-10-26 11:19:05 +02:00
|
|
|
public async delete(viewMotion: ViewMotion): Promise<void> {
|
2018-11-05 17:40:32 +01:00
|
|
|
return await this.dataSend.deleteModel(viewMotion.motion);
|
2018-09-04 14:55:07 +02:00
|
|
|
}
|
2018-09-28 15:10:48 +02:00
|
|
|
|
2018-11-05 17:43:44 +01:00
|
|
|
/**
|
|
|
|
* Set the state of a motion
|
|
|
|
*
|
|
|
|
* @param viewMotion target motion
|
|
|
|
* @param stateId the number that indicates the state
|
|
|
|
*/
|
|
|
|
public async setState(viewMotion: ViewMotion, stateId: number): Promise<void> {
|
|
|
|
const restPath = `/rest/motions/motion/${viewMotion.id}/set_state/`;
|
|
|
|
await this.httpService.put(restPath, { state: stateId });
|
|
|
|
}
|
|
|
|
|
2019-01-18 18:25:02 +01:00
|
|
|
/**
|
|
|
|
* Set the state of motions in bulk
|
|
|
|
*
|
|
|
|
* @param viewMotion target motion
|
|
|
|
* @param stateId the number that indicates the state
|
|
|
|
*/
|
|
|
|
public async setMultiState(viewMotions: ViewMotion[], stateId: number): Promise<void> {
|
|
|
|
const restPath = `/rest/motions/motion/manage_multiple_state/`;
|
|
|
|
const motionsIdMap: { id: number; state: number }[] = viewMotions.map(motion => {
|
|
|
|
return { id: motion.id, state: stateId };
|
|
|
|
});
|
|
|
|
await this.httpService.post(restPath, { motions: motionsIdMap });
|
|
|
|
}
|
|
|
|
|
2018-11-05 17:43:44 +01:00
|
|
|
/**
|
|
|
|
* Set the recommenders state of a motion
|
|
|
|
*
|
|
|
|
* @param viewMotion target motion
|
2018-11-23 13:59:14 +01:00
|
|
|
* @param recommendationId the number that indicates the recommendation
|
2018-11-05 17:43:44 +01:00
|
|
|
*/
|
2018-11-23 13:59:14 +01:00
|
|
|
public async setRecommendation(viewMotion: ViewMotion, recommendationId: number): Promise<void> {
|
2018-11-05 17:43:44 +01:00
|
|
|
const restPath = `/rest/motions/motion/${viewMotion.id}/set_recommendation/`;
|
2018-11-23 13:59:14 +01:00
|
|
|
await this.httpService.put(restPath, { recommendation: recommendationId });
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the category of a motion
|
|
|
|
*
|
|
|
|
* @param viewMotion target motion
|
|
|
|
* @param categoryId the number that indicates the category
|
|
|
|
*/
|
|
|
|
public async setCatetory(viewMotion: ViewMotion, categoryId: number): Promise<void> {
|
|
|
|
const motion = viewMotion.motion;
|
|
|
|
motion.category_id = categoryId;
|
|
|
|
await this.update(motion, viewMotion);
|
2018-11-05 17:43:44 +01:00
|
|
|
}
|
|
|
|
|
2018-12-06 12:28:05 +01:00
|
|
|
/**
|
|
|
|
* Add the motion to a motion block
|
|
|
|
*
|
|
|
|
* @param viewMotion the motion to add
|
|
|
|
* @param blockId the ID of the motion block
|
|
|
|
*/
|
|
|
|
public async setBlock(viewMotion: ViewMotion, blockId: number): Promise<void> {
|
|
|
|
const motion = viewMotion.motion;
|
|
|
|
motion.motion_block_id = blockId;
|
|
|
|
await this.update(motion, viewMotion);
|
|
|
|
}
|
|
|
|
|
2018-12-06 14:58:34 +01:00
|
|
|
/**
|
|
|
|
* Sets the submitters by sending a request to the server,
|
|
|
|
*
|
|
|
|
* @param viewMotion The motion to change the submitters from
|
|
|
|
* @param submitters The submitters to set
|
|
|
|
*/
|
|
|
|
public async setSubmitters(viewMotion: ViewMotion, submitters: User[]): Promise<void> {
|
|
|
|
const requestData = {
|
2019-01-10 12:54:48 +01:00
|
|
|
motions: [
|
|
|
|
{
|
|
|
|
id: viewMotion.id,
|
|
|
|
submitters: submitters.map(s => s.id)
|
|
|
|
}
|
|
|
|
]
|
2018-12-06 14:58:34 +01:00
|
|
|
};
|
|
|
|
this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', requestData);
|
|
|
|
}
|
|
|
|
|
2018-11-08 17:38:44 +01:00
|
|
|
/**
|
2018-11-16 10:25:17 +01:00
|
|
|
* Sends the changed nodes to the server.
|
|
|
|
*
|
|
|
|
* @param data The reordered data from the sorting
|
2018-11-08 17:38:44 +01:00
|
|
|
*/
|
2018-11-22 15:14:01 +01:00
|
|
|
public async sortMotions(data: OSTreeSortEvent<ViewMotion>): Promise<void> {
|
2018-11-08 17:38:44 +01:00
|
|
|
const url = '/rest/motions/motion/sort/';
|
2018-11-16 10:25:17 +01:00
|
|
|
await this.httpService.post(url, data);
|
2018-11-08 17:38:44 +01:00
|
|
|
}
|
|
|
|
|
2018-11-23 13:59:14 +01:00
|
|
|
/**
|
|
|
|
* Supports the motion
|
|
|
|
*
|
|
|
|
* @param viewMotion target motion
|
|
|
|
*/
|
|
|
|
public async support(viewMotion: ViewMotion): Promise<void> {
|
|
|
|
const url = `/rest/motions/motion/${viewMotion.id}/support/`;
|
|
|
|
await this.httpService.post(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unsupports the motion
|
|
|
|
*
|
|
|
|
* @param viewMotion target motion
|
|
|
|
*/
|
|
|
|
public async unsupport(viewMotion: ViewMotion): Promise<void> {
|
|
|
|
const url = `/rest/motions/motion/${viewMotion.id}/support/`;
|
|
|
|
await this.httpService.delete(url);
|
|
|
|
}
|
|
|
|
|
2018-11-04 11:11:48 +01:00
|
|
|
/** Returns an observable returning the amendments to a given motion
|
|
|
|
*
|
|
|
|
* @param {number} motionId
|
|
|
|
* @returns {Observable<ViewMotion[]>}
|
|
|
|
*/
|
|
|
|
public amendmentsTo(motionId: number): Observable<ViewMotion[]> {
|
|
|
|
return this.getViewModelListObservable().pipe(
|
|
|
|
map(
|
|
|
|
(motions: ViewMotion[]): ViewMotion[] => {
|
|
|
|
return motions.filter(
|
|
|
|
(motion: ViewMotion): boolean => {
|
|
|
|
return motion.parent_id === motionId;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-09-28 15:10:48 +02:00
|
|
|
/**
|
|
|
|
* Format the motion text using the line numbering and change
|
|
|
|
* reco algorithm.
|
|
|
|
*
|
|
|
|
* Can be called from detail view and exporter
|
|
|
|
* @param id Motion ID - will be pulled from the repository
|
|
|
|
* @param crMode indicator for the change reco mode
|
2018-09-30 18:43:20 +02:00
|
|
|
* @param changes all change recommendations and amendments, sorted by line number
|
2018-09-09 18:52:47 +02:00
|
|
|
* @param lineLength the current line
|
|
|
|
* @param highlightLine the currently highlighted line (default: none)
|
2018-09-28 15:10:48 +02:00
|
|
|
*/
|
2018-09-30 18:43:20 +02:00
|
|
|
public formatMotion(
|
|
|
|
id: number,
|
|
|
|
crMode: ChangeRecoMode,
|
|
|
|
changes: ViewUnifiedChange[],
|
|
|
|
lineLength: number,
|
|
|
|
highlightLine?: number
|
|
|
|
): string {
|
2018-09-28 15:10:48 +02:00
|
|
|
const targetMotion = this.getViewModel(id);
|
|
|
|
|
|
|
|
if (targetMotion && targetMotion.text) {
|
|
|
|
switch (crMode) {
|
2018-09-30 18:43:20 +02:00
|
|
|
case ChangeRecoMode.Original:
|
|
|
|
return this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength, highlightLine);
|
|
|
|
case ChangeRecoMode.Changed:
|
|
|
|
return this.diff.getTextWithChanges(targetMotion, changes, lineLength, highlightLine);
|
|
|
|
case ChangeRecoMode.Diff:
|
|
|
|
let text = '';
|
|
|
|
changes.forEach((change: ViewUnifiedChange, idx: number) => {
|
|
|
|
if (idx === 0) {
|
|
|
|
text += this.extractMotionLineRange(
|
|
|
|
id,
|
|
|
|
{
|
|
|
|
from: 1,
|
|
|
|
to: change.getLineFrom()
|
|
|
|
},
|
2019-01-17 17:13:34 +01:00
|
|
|
true,
|
|
|
|
lineLength
|
2018-09-30 18:43:20 +02:00
|
|
|
);
|
|
|
|
} else if (changes[idx - 1].getLineTo() < change.getLineFrom()) {
|
|
|
|
text += this.extractMotionLineRange(
|
|
|
|
id,
|
|
|
|
{
|
|
|
|
from: changes[idx - 1].getLineTo(),
|
|
|
|
to: change.getLineFrom()
|
|
|
|
},
|
2019-01-17 17:13:34 +01:00
|
|
|
true,
|
|
|
|
lineLength
|
2018-09-30 18:43:20 +02:00
|
|
|
);
|
|
|
|
}
|
2019-01-17 17:13:34 +01:00
|
|
|
text += this.getChangeDiff(targetMotion, change, lineLength, highlightLine);
|
2018-09-30 18:43:20 +02:00
|
|
|
});
|
2019-01-17 17:13:34 +01:00
|
|
|
text += this.getTextRemainderAfterLastChange(targetMotion, changes, lineLength, highlightLine);
|
2018-09-30 18:43:20 +02:00
|
|
|
return text;
|
|
|
|
case ChangeRecoMode.Final:
|
|
|
|
const appliedChanges: ViewUnifiedChange[] = changes.filter(change => change.isAccepted());
|
|
|
|
return this.diff.getTextWithChanges(targetMotion, appliedChanges, lineLength, highlightLine);
|
|
|
|
default:
|
2018-12-22 19:23:13 +01:00
|
|
|
console.error('unrecognized ChangeRecoMode option (' + crMode + ')');
|
2018-09-30 18:43:20 +02:00
|
|
|
return null;
|
2018-09-28 15:10:48 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2018-09-30 18:43:20 +02:00
|
|
|
|
2018-11-16 10:25:17 +01:00
|
|
|
public formatStatuteAmendment(
|
|
|
|
paragraphs: ViewStatuteParagraph[],
|
|
|
|
amendment: ViewMotion,
|
|
|
|
lineLength: number
|
|
|
|
): string {
|
2018-10-28 11:06:36 +01:00
|
|
|
const origParagraph = paragraphs.find(paragraph => paragraph.id === amendment.statute_paragraph_id);
|
|
|
|
let diffHtml = this.diff.diff(origParagraph.text, amendment.text);
|
|
|
|
diffHtml = this.lineNumbering.insertLineBreaksWithoutNumbers(diffHtml, lineLength, true);
|
|
|
|
return diffHtml;
|
|
|
|
}
|
|
|
|
|
2018-09-30 18:43:20 +02:00
|
|
|
/**
|
|
|
|
* Extracts a renderable HTML string representing the given line number range of this motion
|
|
|
|
*
|
|
|
|
* @param {number} id
|
|
|
|
* @param {LineRange} lineRange
|
|
|
|
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-09-30 18:43:20 +02:00
|
|
|
*/
|
2019-01-17 17:13:34 +01:00
|
|
|
public extractMotionLineRange(id: number, lineRange: LineRange, lineNumbers: boolean, lineLength: number): string {
|
|
|
|
const origHtml = this.formatMotion(id, ChangeRecoMode.Original, [], lineLength);
|
2018-09-30 18:43:20 +02:00
|
|
|
const extracted = this.diff.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
|
|
|
|
let html =
|
|
|
|
extracted.outerContextStart +
|
|
|
|
extracted.innerContextStart +
|
|
|
|
extracted.html +
|
|
|
|
extracted.innerContextEnd +
|
|
|
|
extracted.outerContextEnd;
|
|
|
|
if (lineNumbers) {
|
2019-01-17 17:13:34 +01:00
|
|
|
html = this.lineNumbering.insertLineNumbers(html, lineLength, null, null, lineRange.from);
|
2018-09-30 18:43:20 +02:00
|
|
|
}
|
|
|
|
return html;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the remainder text of the motion after the last change
|
|
|
|
*
|
|
|
|
* @param {ViewMotion} motion
|
|
|
|
* @param {ViewUnifiedChange[]} changes
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-09-30 18:43:20 +02:00
|
|
|
* @param {number} highlight
|
2018-11-04 11:11:48 +01:00
|
|
|
* @returns {string}
|
2018-09-30 18:43:20 +02:00
|
|
|
*/
|
|
|
|
public getTextRemainderAfterLastChange(
|
|
|
|
motion: ViewMotion,
|
|
|
|
changes: ViewUnifiedChange[],
|
2019-01-17 17:13:34 +01:00
|
|
|
lineLength: number,
|
2018-09-30 18:43:20 +02:00
|
|
|
highlight?: number
|
|
|
|
): string {
|
2018-12-22 19:23:13 +01:00
|
|
|
let maxLine = 1;
|
2018-09-30 18:43:20 +02:00
|
|
|
changes.forEach((change: ViewUnifiedChange) => {
|
|
|
|
if (change.getLineTo() > maxLine) {
|
|
|
|
maxLine = change.getLineTo();
|
|
|
|
}
|
|
|
|
}, 0);
|
|
|
|
|
2019-01-17 17:13:34 +01:00
|
|
|
const numberedHtml = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
2018-12-22 19:23:13 +01:00
|
|
|
if (changes.length === 0) {
|
|
|
|
return numberedHtml;
|
|
|
|
}
|
|
|
|
|
2018-09-30 18:43:20 +02:00
|
|
|
let data;
|
|
|
|
|
|
|
|
try {
|
|
|
|
data = this.diff.extractRangeByLineNumbers(numberedHtml, maxLine, null);
|
|
|
|
} catch (e) {
|
|
|
|
// This only happens (as far as we know) when the motion text has been altered (shortened)
|
|
|
|
// without modifying the change recommendations accordingly.
|
|
|
|
// That's a pretty serious inconsistency that should not happen at all,
|
|
|
|
// we're just doing some basic damage control here.
|
|
|
|
const msg =
|
|
|
|
'Inconsistent data. A change recommendation is probably referring to a non-existant line number.';
|
|
|
|
return '<em style="color: red; font-weight: bold;">' + msg + '</em>';
|
|
|
|
}
|
|
|
|
|
|
|
|
let html;
|
|
|
|
if (data.html !== '') {
|
|
|
|
// Add "merge-before"-css-class if the first line begins in the middle of a paragraph. Used for PDF.
|
|
|
|
html =
|
|
|
|
this.diff.addCSSClassToFirstTag(data.outerContextStart + data.innerContextStart, 'merge-before') +
|
|
|
|
data.html +
|
|
|
|
data.innerContextEnd +
|
|
|
|
data.outerContextEnd;
|
2019-01-17 17:13:34 +01:00
|
|
|
html = this.lineNumbering.insertLineNumbers(html, lineLength, highlight, null, maxLine);
|
2018-09-30 18:43:20 +02:00
|
|
|
} else {
|
|
|
|
// Prevents empty lines at the end of the motion
|
|
|
|
html = '';
|
|
|
|
}
|
|
|
|
return html;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a {@link ViewChangeReco} object based on the motion ID and the given lange range.
|
|
|
|
* This object is not saved yet and does not yet have any changed HTML. It's meant to populate the UI form.
|
|
|
|
*
|
|
|
|
* @param {number} motionId
|
|
|
|
* @param {LineRange} lineRange
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-09-30 18:43:20 +02:00
|
|
|
*/
|
2019-01-17 17:13:34 +01:00
|
|
|
public createChangeRecommendationTemplate(
|
|
|
|
motionId: number,
|
|
|
|
lineRange: LineRange,
|
|
|
|
lineLength: number
|
|
|
|
): ViewChangeReco {
|
2018-09-30 18:43:20 +02:00
|
|
|
const changeReco = new MotionChangeReco();
|
|
|
|
changeReco.line_from = lineRange.from;
|
|
|
|
changeReco.line_to = lineRange.to;
|
|
|
|
changeReco.type = ModificationType.TYPE_REPLACEMENT;
|
2019-01-17 17:13:34 +01:00
|
|
|
changeReco.text = this.extractMotionLineRange(motionId, lineRange, false, lineLength);
|
2018-09-30 18:43:20 +02:00
|
|
|
changeReco.rejected = false;
|
|
|
|
changeReco.motion_id = motionId;
|
|
|
|
|
|
|
|
return new ViewChangeReco(changeReco);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the HTML with the changes, optionally with a highlighted line.
|
|
|
|
* The original motion needs to be provided.
|
|
|
|
*
|
|
|
|
* @param {ViewMotion} motion
|
|
|
|
* @param {ViewUnifiedChange} change
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-09-30 18:43:20 +02:00
|
|
|
* @param {number} highlight
|
2018-11-04 11:11:48 +01:00
|
|
|
* @returns {string}
|
2018-09-30 18:43:20 +02:00
|
|
|
*/
|
2019-01-17 17:13:34 +01:00
|
|
|
public getChangeDiff(
|
|
|
|
motion: ViewMotion,
|
|
|
|
change: ViewUnifiedChange,
|
|
|
|
lineLength: number,
|
|
|
|
highlight?: number
|
|
|
|
): string {
|
|
|
|
const html = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
2018-09-30 18:43:20 +02:00
|
|
|
|
|
|
|
let data, oldText;
|
|
|
|
|
|
|
|
try {
|
|
|
|
data = this.diff.extractRangeByLineNumbers(html, change.getLineFrom(), change.getLineTo());
|
|
|
|
oldText =
|
|
|
|
data.outerContextStart +
|
|
|
|
data.innerContextStart +
|
|
|
|
data.html +
|
|
|
|
data.innerContextEnd +
|
|
|
|
data.outerContextEnd;
|
|
|
|
} catch (e) {
|
|
|
|
// This only happens (as far as we know) when the motion text has been altered (shortened)
|
|
|
|
// without modifying the change recommendations accordingly.
|
|
|
|
// That's a pretty serious inconsistency that should not happen at all,
|
|
|
|
// we're just doing some basic damage control here.
|
|
|
|
const msg =
|
|
|
|
'Inconsistent data. A change recommendation is probably referring to a non-existant line number.';
|
|
|
|
return '<em style="color: red; font-weight: bold;">' + msg + '</em>';
|
|
|
|
}
|
|
|
|
|
|
|
|
oldText = this.lineNumbering.insertLineNumbers(oldText, lineLength, null, null, change.getLineFrom());
|
|
|
|
let diff = this.diff.diff(oldText, change.getChangeNewText());
|
|
|
|
|
|
|
|
// If an insertion makes the line longer than the line length limit, we need two line breaking runs:
|
|
|
|
// - First, for the official line numbers, ignoring insertions (that's been done some lines before)
|
|
|
|
// - Second, another one to prevent the displayed including insertions to exceed the page width
|
|
|
|
diff = this.lineNumbering.insertLineBreaksWithoutNumbers(diff, lineLength, true);
|
|
|
|
|
|
|
|
if (highlight > 0) {
|
|
|
|
diff = this.lineNumbering.highlightLine(diff, highlight);
|
|
|
|
}
|
|
|
|
|
|
|
|
const origBeginning = data.outerContextStart + data.innerContextStart;
|
|
|
|
if (diff.toLowerCase().indexOf(origBeginning.toLowerCase()) === 0) {
|
|
|
|
// Add "merge-before"-css-class if the first line begins in the middle of a paragraph. Used for PDF.
|
|
|
|
diff =
|
|
|
|
this.diff.addCSSClassToFirstTag(origBeginning, 'merge-before') + diff.substring(origBeginning.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
return diff;
|
|
|
|
}
|
2018-11-04 11:11:48 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an amendment, this returns the motion affected by this amendments
|
|
|
|
*
|
|
|
|
* @param {ViewMotion} amendment
|
|
|
|
* @returns {ViewMotion}
|
|
|
|
*/
|
|
|
|
public getAmendmentBaseMotion(amendment: ViewMotion): ViewMotion {
|
|
|
|
return this.getViewModel(amendment.parent_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Splits a motion into paragraphs, optionally adding line numbers
|
|
|
|
*
|
|
|
|
* @param {ViewMotion} motion
|
|
|
|
* @param {boolean} lineBreaks
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-11-04 11:11:48 +01:00
|
|
|
* @returns {string[]}
|
|
|
|
*/
|
2019-01-17 17:13:34 +01:00
|
|
|
public getTextParagraphs(motion: ViewMotion, lineBreaks: boolean, lineLength: number): string[] {
|
2018-11-04 11:11:48 +01:00
|
|
|
if (!motion) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
let html = motion.text;
|
|
|
|
if (lineBreaks) {
|
|
|
|
html = this.lineNumbering.insertLineNumbers(html, lineLength);
|
|
|
|
}
|
|
|
|
return this.lineNumbering.splitToParagraphs(html);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all paragraphs that are affected by the given amendment in diff-format
|
|
|
|
*
|
|
|
|
* @param {ViewMotion} amendment
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-11-04 11:11:48 +01:00
|
|
|
* @returns {DiffLinesInParagraph}
|
|
|
|
*/
|
2019-01-17 17:13:34 +01:00
|
|
|
public getAmendedParagraphs(amendment: ViewMotion, lineLength: number): DiffLinesInParagraph[] {
|
2018-11-04 11:11:48 +01:00
|
|
|
const motion = this.getAmendmentBaseMotion(amendment);
|
2019-01-17 17:13:34 +01:00
|
|
|
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
2018-11-04 11:11:48 +01:00
|
|
|
|
|
|
|
return amendment.amendment_paragraphs
|
|
|
|
.map(
|
|
|
|
(newText: string, paraNo: number): DiffLinesInParagraph => {
|
|
|
|
if (newText === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Hint: can be either DiffLinesInParagraph or null, if no changes are made
|
|
|
|
return this.diff.getAmendmentParagraphsLinesByMode(
|
|
|
|
paraNo,
|
|
|
|
baseParagraphs[paraNo],
|
|
|
|
newText,
|
|
|
|
lineLength
|
|
|
|
);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
.filter((para: DiffLinesInParagraph) => para !== null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all paragraphs that are affected by the given amendment as unified change objects.
|
|
|
|
*
|
|
|
|
* @param {ViewMotion} amendment
|
2019-01-17 17:13:34 +01:00
|
|
|
* @param {number} lineLength
|
2018-11-04 11:11:48 +01:00
|
|
|
* @returns {ViewMotionAmendedParagraph[]}
|
|
|
|
*/
|
2019-01-17 17:13:34 +01:00
|
|
|
public getAmendmentAmendedParagraphs(amendment: ViewMotion, lineLength: number): ViewMotionAmendedParagraph[] {
|
2018-11-04 11:11:48 +01:00
|
|
|
const motion = this.getAmendmentBaseMotion(amendment);
|
2019-01-17 17:13:34 +01:00
|
|
|
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
2018-11-04 11:11:48 +01:00
|
|
|
|
|
|
|
return amendment.amendment_paragraphs
|
|
|
|
.map(
|
|
|
|
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
|
|
|
|
if (newText === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const origText = baseParagraphs[paraNo],
|
|
|
|
paragraphLines = this.lineNumbering.getLineNumberRange(origText),
|
|
|
|
diff = this.diff.diff(origText, newText),
|
|
|
|
affectedLines = this.diff.detectAffectedLineRange(diff);
|
|
|
|
|
|
|
|
if (affectedLines === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let newTextLines = this.lineNumbering.insertLineNumbers(
|
|
|
|
newText,
|
|
|
|
lineLength,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
paragraphLines.from
|
|
|
|
);
|
|
|
|
newTextLines = this.diff.formatDiff(
|
|
|
|
this.diff.extractRangeByLineNumbers(newTextLines, affectedLines.from, affectedLines.to)
|
|
|
|
);
|
|
|
|
|
|
|
|
return new ViewMotionAmendedParagraph(amendment, paraNo, newTextLines, affectedLines);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
.filter((para: ViewMotionAmendedParagraph) => para !== null);
|
|
|
|
}
|
2018-12-04 19:31:24 +01:00
|
|
|
|
|
|
|
/**
|
2019-01-11 18:55:09 +01:00
|
|
|
* Returns motion duplicates (sharing the identifier)
|
|
|
|
*
|
2018-12-04 19:31:24 +01:00
|
|
|
* @param viewMotion the ViewMotion to compare against the list of Motions
|
|
|
|
* in the data
|
2019-01-11 18:55:09 +01:00
|
|
|
* @returns An Array of ViewMotions with the same identifier of the input, or an empty array
|
2018-12-04 19:31:24 +01:00
|
|
|
*/
|
2019-01-11 18:55:09 +01:00
|
|
|
public getMotionDuplicates(motion: ViewMotion): ViewMotion[] {
|
2018-12-04 19:31:24 +01:00
|
|
|
const duplicates = this.DS.filter(Motion, item => motion.identifier === item.identifier);
|
|
|
|
const viewMotions: ViewMotion[] = [];
|
|
|
|
duplicates.forEach(item => viewMotions.push(this.createViewModel(item)));
|
|
|
|
return viewMotions;
|
|
|
|
}
|
2018-12-21 15:05:11 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a request to the server, creating a new poll for the motion
|
|
|
|
*/
|
|
|
|
public async createPoll(motion: ViewMotion): Promise<void> {
|
|
|
|
const url = '/rest/motions/motion/' + motion.id + '/create_poll/';
|
|
|
|
await this.httpService.post(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends an update request for a poll.
|
|
|
|
*
|
|
|
|
* @param poll
|
|
|
|
*/
|
|
|
|
public async updatePoll(poll: MotionPoll): Promise<void> {
|
|
|
|
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
|
|
|
const data = {
|
|
|
|
motion_id: poll.motion_id,
|
|
|
|
id: poll.id,
|
|
|
|
votescast: poll.votescast,
|
|
|
|
votesvalid: poll.votesvalid,
|
|
|
|
votesinvalid: poll.votesinvalid,
|
|
|
|
votes: {
|
|
|
|
Yes: poll.yes,
|
|
|
|
No: poll.no,
|
|
|
|
Abstain: poll.abstain
|
|
|
|
}
|
|
|
|
};
|
|
|
|
await this.httpService.put(url, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-01-17 10:53:16 +01:00
|
|
|
* Sends a http request to delete the given poll
|
2018-12-21 15:05:11 +01:00
|
|
|
*
|
|
|
|
* @param poll
|
|
|
|
*/
|
|
|
|
public async deletePoll(poll: MotionPoll): Promise<void> {
|
|
|
|
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
|
|
|
await this.httpService.delete(url);
|
|
|
|
}
|
2019-01-17 10:53:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Signals the acceptance of the current recommendation to the server
|
|
|
|
*
|
|
|
|
* @param motion A ViewMotion
|
|
|
|
*/
|
|
|
|
public async followRecommendation(motion: ViewMotion): Promise<void> {
|
|
|
|
if (motion.recommendation_id) {
|
|
|
|
const restPath = `/rest/motions/motion/${motion.id}/follow_recommendation/`;
|
|
|
|
await this.httpService.post(restPath);
|
|
|
|
}
|
|
|
|
}
|
2019-01-18 17:13:31 +01:00
|
|
|
/**
|
|
|
|
* Check if a motion currently has any amendments
|
|
|
|
*
|
|
|
|
* @param motion A viewMotion
|
|
|
|
* @returns True if there is at eleast one amendment
|
|
|
|
*/
|
|
|
|
public hasAmendments(motion: ViewMotion): boolean {
|
|
|
|
return this.getViewModelList().filter(allMotions => allMotions.parent_id === motion.id).length > 0;
|
|
|
|
}
|
2019-01-21 16:34:19 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* updates the state Extension with the string given, if the current workflow allows for it
|
|
|
|
*
|
|
|
|
* @param viewMotion
|
|
|
|
* @param value
|
|
|
|
*/
|
|
|
|
public async setStateExtension(viewMotion: ViewMotion, value: string): Promise<void> {
|
|
|
|
if (viewMotion.state.show_state_extension_field) {
|
|
|
|
return this.update({ state_extension: value }, viewMotion);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* updates the recommendation extension with the string given, if the current workflow allows for it
|
|
|
|
*
|
|
|
|
* @param viewMotion
|
|
|
|
* @param value
|
|
|
|
*/
|
|
|
|
public async setRecommendationExtension(viewMotion: ViewMotion, value: string): Promise<void> {
|
|
|
|
if (viewMotion.recommendation.show_recommendation_extension_field) {
|
|
|
|
return this.update({ recommendation_extension: value }, viewMotion);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the label for the motion's current state with the extension
|
|
|
|
* attached (if present). For cross-referencing other motions, `[motion:id]`
|
|
|
|
* will replaced by the referenced motion's identifier (see {@link solveExtensionPlaceHolder})
|
|
|
|
*
|
|
|
|
* @param motion
|
|
|
|
* @returns the translated state with the extension attached
|
|
|
|
*/
|
|
|
|
public getExtendedStateLabel(motion: ViewMotion): string {
|
|
|
|
let rec = this.translate.instant(motion.state.name);
|
|
|
|
if (motion.stateExtension && motion.state.show_state_extension_field) {
|
|
|
|
rec += ' ' + this.solveExtensionPlaceHolder(motion.stateExtension);
|
|
|
|
}
|
|
|
|
return rec;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the label for the motion's current recommendation with the extension
|
|
|
|
* attached (if present)
|
|
|
|
*
|
|
|
|
* @param motion
|
|
|
|
* @returns the translated extension with the extension attached, 'not set'
|
|
|
|
* if no recommendation si set
|
|
|
|
*/
|
|
|
|
public getExtendedRecommendationLabel(motion: ViewMotion): string {
|
|
|
|
if (!motion.recommendation) {
|
|
|
|
return this.translate.instant('not set');
|
|
|
|
}
|
|
|
|
let rec = this.translate.instant(motion.recommendation.recommendation_label);
|
|
|
|
if (motion.recommendationExtension && motion.recommendation.show_recommendation_extension_field) {
|
|
|
|
rec += ' ' + this.solveExtensionPlaceHolder(motion.recommendationExtension);
|
|
|
|
}
|
|
|
|
return rec;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces any motion placeholder (`[motion:id]`) with the motion's title(s)
|
|
|
|
*
|
|
|
|
* @param value
|
|
|
|
* @returns the string with the motion titles replacing the placeholders, '??' strings for errors
|
|
|
|
*/
|
|
|
|
public solveExtensionPlaceHolder(value: string): string {
|
|
|
|
const beg = value.indexOf('[motion:');
|
|
|
|
const end = value.indexOf(']');
|
|
|
|
if (beg > -1 && (end > -1 && end > beg)) {
|
|
|
|
const id = Number(value.substring(beg + 8, end));
|
|
|
|
const referedMotion = Number.isNaN(id) ? null : this.getViewModel(id);
|
|
|
|
const title = referedMotion ? referedMotion.identifier : '??';
|
|
|
|
value = value.substring(0, beg) + title + value.substring(end + 1);
|
|
|
|
// recursively check for additional occurrences
|
|
|
|
return this.solveExtensionPlaceHolder(value);
|
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
2018-09-04 14:55:07 +02:00
|
|
|
}
|