Change recommendations for amendments

This commit is contained in:
Tobias Hößl 2020-04-04 10:11:56 +02:00
parent e0069f734a
commit b51787129b
No known key found for this signature in database
GPG Key ID: 1D780C7599C2D2A2
13 changed files with 548 additions and 157 deletions

View File

@ -18,6 +18,7 @@ import {
import { ChangeRecoMode } from 'app/site/motions/motions.constants'; import { ChangeRecoMode } from 'app/site/motions/motions.constants';
import { BaseRepository } from '../base-repository'; import { BaseRepository } from '../base-repository';
import { DiffService, LineRange, ModificationType } from '../../ui-services/diff.service'; import { DiffService, LineRange, ModificationType } from '../../ui-services/diff.service';
import { LinenumberingService } from '../../ui-services/linenumbering.service';
import { ViewMotion } from '../../../site/motions/models/view-motion'; import { ViewMotion } from '../../../site/motions/models/view-motion';
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
@ -50,7 +51,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
* @param {CollectionStringMapperService} mapperService Maps collection strings to classes * @param {CollectionStringMapperService} mapperService Maps collection strings to classes
* @param {ViewModelStoreService} viewModelStoreService * @param {ViewModelStoreService} viewModelStoreService
* @param {TranslateService} translate * @param {TranslateService} translate
* @param {RelationManagerService} relationManager
* @param {DiffService} diffService * @param {DiffService} diffService
* @param {LinenumberingService} lineNumbering Line numbering service
*/ */
public constructor( public constructor(
DS: DataStoreService, DS: DataStoreService,
@ -59,7 +62,8 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService,
relationManager: RelationManagerService, relationManager: RelationManagerService,
private diffService: DiffService private diffService: DiffService,
private lineNumbering: LinenumberingService
) { ) {
super( super(
DS, DS,
@ -103,7 +107,7 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
/** /**
* Synchronously getting the change recommendations of the corresponding motion. * Synchronously getting the change recommendations of the corresponding motion.
* *
* @param motionId the id of the target motion * @param motion_id the id of the target motion
* @returns the array of change recommendations to the motions. * @returns the array of change recommendations to the motions.
*/ */
public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] { public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] {
@ -171,22 +175,61 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
* @param {LineRange} lineRange * @param {LineRange} lineRange
* @param {number} lineLength * @param {number} lineLength
*/ */
public createChangeRecommendationTemplate( public createMotionChangeRecommendationTemplate(
motion: ViewMotion, motion: ViewMotion,
lineRange: LineRange, lineRange: LineRange,
lineLength: number lineLength: number
): ViewMotionChangeRecommendation { ): ViewMotionChangeRecommendation {
const motionText = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
const changeReco = new MotionChangeRecommendation(); const changeReco = new MotionChangeRecommendation();
changeReco.line_from = lineRange.from; changeReco.line_from = lineRange.from;
changeReco.line_to = lineRange.to; changeReco.line_to = lineRange.to;
changeReco.type = ModificationType.TYPE_REPLACEMENT; changeReco.type = ModificationType.TYPE_REPLACEMENT;
changeReco.text = this.diffService.extractMotionLineRange(motion.text, lineRange, false, lineLength, null); changeReco.text = this.diffService.extractMotionLineRange(motionText, lineRange, false, lineLength, null);
changeReco.rejected = false; changeReco.rejected = false;
changeReco.motion_id = motion.id; changeReco.motion_id = motion.id;
return new ViewMotionChangeRecommendation(changeReco); return new ViewMotionChangeRecommendation(changeReco);
} }
/**
* Creates a {@link ViewMotionChangeRecommendation} object based on the amendment ID, the precalculated
* paragraphs (because we don't have access to motion-repository serice here) 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 {ViewMotion} amendment
* @param {string[]} lineNumberedParagraphs
* @param {LineRange} lineRange
* @param {number} lineLength
*/
public createAmendmentChangeRecommendationTemplate(
amendment: ViewMotion,
lineNumberedParagraphs: string[],
lineRange: LineRange,
lineLength: number
): ViewMotionChangeRecommendation {
const consolidatedText = lineNumberedParagraphs.join('\n');
const extracted = this.diffService.extractRangeByLineNumbers(consolidatedText, lineRange.from, lineRange.to);
const extractedHtml =
extracted.outerContextStart +
extracted.innerContextStart +
extracted.html +
extracted.innerContextEnd +
extracted.outerContextEnd;
const changeReco = new MotionChangeRecommendation();
changeReco.line_from = lineRange.from;
changeReco.line_to = lineRange.to;
changeReco.type = ModificationType.TYPE_REPLACEMENT;
changeReco.rejected = false;
changeReco.motion_id = amendment.id;
changeReco.text = extractedHtml;
return new ViewMotionChangeRecommendation(changeReco);
}
/** /**
* Creates a {@link ViewMotionChangeRecommendation} object to change the title, based on the motion ID. * Creates a {@link ViewMotionChangeRecommendation} object to change the title, based on the motion ID.
* This object is not saved yet and does not yet have any changed title. It's meant to populate the UI form. * This object is not saved yet and does not yet have any changed title. It's meant to populate the UI form.

View File

@ -37,7 +37,7 @@ import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../bas
import { NestedModelDescriptors } from '../base-repository'; import { NestedModelDescriptors } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service'; import { LineNumberedString, LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
type SortProperty = 'weight' | 'identifier'; type SortProperty = 'weight' | 'identifier';
@ -201,11 +201,14 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
* @param DS The DataStore * @param DS The DataStore
* @param mapperService Maps collection strings to classes * @param mapperService Maps collection strings to classes
* @param dataSend sending changed objects * @param dataSend sending changed objects
* @param viewModelStoreService ViewModelStoreService
* @param translate
* @param relationManager
* @param httpService OpenSlides own Http service * @param httpService OpenSlides own Http service
* @param lineNumbering Line numbering for motion text * @param lineNumbering Line numbering for motion text
* @param diff Display changes in motion text as diff. * @param diff Display changes in motion text as diff.
* @param personalNoteService service fo personal notes
* @param config ConfigService (subscribe to sorting config) * @param config ConfigService (subscribe to sorting config)
* @param operator
*/ */
public constructor( public constructor(
DS: DataStoreService, DS: DataStoreService,
@ -322,7 +325,16 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
ownKey: 'diffLines', ownKey: 'diffLines',
get: (motion: Motion, viewMotion: ViewMotion) => { get: (motion: Motion, viewMotion: ViewMotion) => {
if (viewMotion.parent) { if (viewMotion.parent) {
return this.getAmendmentParagraphs(viewMotion, this.motionLineLength, false); const changeRecos = viewMotion.changeRecommendations.filter(changeReco =>
changeReco.showInFinalView()
);
return this.getAmendmentParagraphLines(
viewMotion,
this.motionLineLength,
ChangeRecoMode.Changed,
changeRecos,
false
);
} }
}, },
getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent
@ -376,7 +388,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
/** /**
* Set the state of motions in bulk * Set the state of motions in bulk
* *
* @param viewMotion target motion * @param viewMotions target motions
* @param stateId the number that indicates the state * @param stateId the number that indicates the state
*/ */
public async setMultiState(viewMotions: ViewMotion[], stateId: number): Promise<void> { public async setMultiState(viewMotions: ViewMotion[], stateId: number): Promise<void> {
@ -390,7 +402,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
/** /**
* Set the motion blocks of motions in bulk * Set the motion blocks of motions in bulk
* *
* @param viewMotion target motion * @param viewMotions target motions
* @param motionblockId the number that indicates the motion block * @param motionblockId the number that indicates the motion block
*/ */
public async setMultiMotionBlock(viewMotions: ViewMotion[], motionblockId: number): Promise<void> { public async setMultiMotionBlock(viewMotions: ViewMotion[], motionblockId: number): Promise<void> {
@ -404,7 +416,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
/** /**
* Set the category of motions in bulk * Set the category of motions in bulk
* *
* @param viewMotion target motion * @param viewMotions target motions
* @param categoryId the number that indicates the category * @param categoryId the number that indicates the category
*/ */
public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise<void> { public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise<void> {
@ -609,11 +621,12 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
case ChangeRecoMode.Diff: case ChangeRecoMode.Diff:
const text = []; const text = [];
const changesToShow = changes.filter(change => change.showInDiffView()); const changesToShow = changes.filter(change => change.showInDiffView());
const motionText = this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength);
for (let i = 0; i < changesToShow.length; i++) { for (let i = 0; i < changesToShow.length; i++) {
text.push( text.push(
this.diff.extractMotionLineRange( this.diff.extractMotionLineRange(
targetMotion.text, motionText,
{ {
from: i === 0 ? 1 : changesToShow[i - 1].getLineTo(), from: i === 0 ? 1 : changesToShow[i - 1].getLineTo(),
to: changesToShow[i].getLineFrom() to: changesToShow[i].getLineFrom()
@ -624,18 +637,11 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
) )
); );
text.push( text.push(this.diff.getChangeDiff(motionText, changesToShow[i], lineLength, highlightLine));
this.diff.getChangeDiff(targetMotion.text, changesToShow[i], lineLength, highlightLine)
);
} }
text.push( text.push(
this.diff.getTextRemainderAfterLastChange( this.diff.getTextRemainderAfterLastChange(motionText, changesToShow, lineLength, highlightLine)
targetMotion.text,
changesToShow,
lineLength,
highlightLine
)
); );
return text.join(''); return text.join('');
case ChangeRecoMode.Final: case ChangeRecoMode.Final:
@ -760,66 +766,173 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
} }
/** /**
* Returns all paragraphs that are affected by the given amendment in diff-format * Returns the amended paragraphs by an amendment. Correlates to the amendment_paragraphs field,
* but also considers relevant change recommendations.
* The returned array includes "null" values for paragraphs that have not been changed.
* *
* @param {ViewMotion} amendment * @param {ViewMotion} amendment
* @param {number} lineLength * @param {number} lineLength
* @param {ViewMotionChangeRecommendation[]} changes
* @param {boolean} includeUnchanged
* @returns {string[]}
*/
public applyChangesToAmendment(
amendment: ViewMotion,
lineLength: number,
changes: ViewMotionChangeRecommendation[],
includeUnchanged: boolean
): string[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
// Changes need to be applied from the bottom up, to prevent conflicts with changing line numbers.
changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => {
if (change1.getLineFrom() < change2.getLineFrom()) {
return 1;
} else if (change1.getLineFrom() > change2.getLineFrom()) {
return -1;
} else {
return 0;
}
});
return amendment.amendment_paragraphs.map((newText: string, paraNo: number) => {
let paragraph: string;
let paragraphHasChanges;
if (newText === null) {
paragraph = baseParagraphs[paraNo];
paragraphHasChanges = false;
} else {
// Add line numbers to newText, relative to the baseParagraph, by creating a diff
// to the line numbered base version any applying it right away
const diff = this.diff.diff(baseParagraphs[paraNo], newText);
paragraph = this.diff.diffHtmlToFinalText(diff);
paragraphHasChanges = true;
}
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
changes.forEach((change: ViewMotionChangeRecommendation) => {
// Hint: this assumes that change recommendations only affect one specific paragraph, not multiple
if (change.line_from >= affected.from && change.line_from < affected.to) {
paragraph = this.diff.replaceLines(paragraph, change.text, change.line_from, change.line_to);
// Reapply relative line numbers
const diff = this.diff.diff(baseParagraphs[paraNo], paragraph);
paragraph = this.diff.diffHtmlToFinalText(diff);
paragraphHasChanges = true;
}
});
if (paragraphHasChanges || includeUnchanged) {
return paragraph;
} else {
return null;
}
});
}
/**
* Returns all paragraph lines that are affected by the given amendment in diff-format, including context
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {ChangeRecoMode} crMode
* @param {ViewMotionChangeRecommendation[]} changeRecommendations
* @param {boolean} includeUnchanged * @param {boolean} includeUnchanged
* @returns {DiffLinesInParagraph} * @returns {DiffLinesInParagraph}
*/ */
public getAmendmentParagraphs( public getAmendmentParagraphLines(
amendment: ViewMotion, amendment: ViewMotion,
lineLength: number, lineLength: number,
crMode: ChangeRecoMode,
changeRecommendations: ViewMotionChangeRecommendation[],
includeUnchanged: boolean includeUnchanged: boolean
): DiffLinesInParagraph[] { ): DiffLinesInParagraph[] {
const motion = amendment.parent; const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength); const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return (amendment.amendment_paragraphs || []) let amendmentParagraphs;
if (crMode === ChangeRecoMode.Changed) {
amendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecommendations, true);
} else {
amendmentParagraphs = amendment.amendment_paragraphs || [];
}
return amendmentParagraphs
.map( .map(
(newText: string, paraNo: number): DiffLinesInParagraph => { (newText: string, paraNo: number): DiffLinesInParagraph => {
if (newText !== null) { if (newText !== null) {
return this.diff.getAmendmentParagraphsLinesByMode( return this.diff.getAmendmentParagraphsLines(
paraNo, paraNo,
baseParagraphs[paraNo], baseParagraphs[paraNo],
newText, newText,
lineLength lineLength
); );
} else { } else {
// Nothing has changed in this paragraph return null; // Nothing has changed in this paragraph
if (includeUnchanged) {
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
return {
paragraphNo: paraNo,
paragraphLineFrom: paragraph_line_range.from,
paragraphLineTo: paragraph_line_range.to,
diffLineFrom: paragraph_line_range.to,
diffLineTo: paragraph_line_range.to,
textPre: baseParagraphs[paraNo],
text: '',
textPost: ''
} as DiffLinesInParagraph;
} else {
return null; // null will make this paragraph filtered out
}
} }
} }
) )
.map((diffLines: DiffLinesInParagraph, paraNo: number) => {
// If nothing has changed and we want to keep unchanged paragraphs for the context,
// return the original text in "textPre"
if (diffLines === null && includeUnchanged) {
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
return {
paragraphNo: paraNo,
paragraphLineFrom: paragraph_line_range.from,
paragraphLineTo: paragraph_line_range.to,
diffLineFrom: paragraph_line_range.to,
diffLineTo: paragraph_line_range.to,
textPre: baseParagraphs[paraNo],
text: '',
textPost: ''
} as DiffLinesInParagraph;
} else {
return diffLines;
}
})
.filter((para: DiffLinesInParagraph) => para !== null); .filter((para: DiffLinesInParagraph) => para !== null);
} }
public getAmendmentParagraphLinesTitle(paragraph: DiffLinesInParagraph): string {
if (paragraph.diffLineTo === paragraph.diffLineFrom + 1) {
return this.translate.instant('Line') + ' ' + paragraph.diffLineFrom.toString(10);
} else {
return (
this.translate.instant('Line') +
' ' +
paragraph.diffLineFrom.toString(10) +
' - ' +
(paragraph.diffLineTo - 1).toString(10)
);
}
}
/** /**
* Returns all paragraphs that are affected by the given amendment as unified change objects. * Returns all paragraphs that are affected by the given amendment as unified change objects.
* Only the affected part of each paragraph is returned.
* Change recommendations to this amendment are considered here, too. That is, if a change recommendation
* for an amendment exists and is not rejected, the changed amendment will be returned here.
* *
* @param {ViewMotion} amendment * @param {ViewMotion} amendment
* @param {number} lineLength * @param {number} lineLength
* @param {ViewMotionChangeRecommendation[]} changeRecos
* @returns {ViewMotionAmendedParagraph[]} * @returns {ViewMotionAmendedParagraph[]}
*/ */
public getAmendmentAmendedParagraphs(amendment: ViewMotion, lineLength: number): ViewMotionAmendedParagraph[] { public getAmendmentAmendedParagraphs(
amendment: ViewMotion,
lineLength: number,
changeRecos: ViewMotionChangeRecommendation[]
): ViewMotionAmendedParagraph[] {
const motion = amendment.parent; const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength); const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
const changedAmendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecos, false);
return (amendment.amendment_paragraphs || []) return changedAmendmentParagraphs
.map( .map(
(newText: string, paraNo: number): ViewMotionAmendedParagraph => { (newText: string, paraNo: number): ViewMotionAmendedParagraph => {
if (newText === null) { if (newText === null) {
@ -844,6 +957,42 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
.filter((para: ViewMotionAmendedParagraph) => para !== null); .filter((para: ViewMotionAmendedParagraph) => para !== null);
} }
/**
* For unchanged paragraphs, this returns the original motion paragraph, including line numbers.
* For changed paragraphs, this returns the content of the amendment_paragraphs-field,
* but including line numbers relative to the original motion line numbers,
* so they can be used for the amendment change recommendations
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {boolean} withDiff
* @returns {LineNumberedString[]}
*/
public getAllAmendmentParagraphsWithOriginalLineNumbers(
amendment: ViewMotion,
lineLength: number,
withDiff: boolean
): LineNumberedString[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return (amendment.amendment_paragraphs || []).map((newText: string, paraNo: number): string => {
const origText = baseParagraphs[paraNo];
if (newText === null) {
return origText;
}
const diff = this.diff.diff(origText, newText);
if (withDiff) {
return diff;
} else {
return this.diff.diffHtmlToFinalText(diff);
}
});
}
/** /**
* Signals the acceptance of the current recommendation to the server * Signals the acceptance of the current recommendation to the server
* *

View File

@ -1172,6 +1172,28 @@ describe('DiffService', () => {
); );
} }
)); ));
it('detects a word replacement at the end of line correctly', inject([DiffService], (service: DiffService) => {
const before =
'<p>' +
noMarkup(1) +
'wuid Brotzeit? Pfenningguat Stubn bitt da, hog di hi fei nia need nia need Goaßmaß ' +
brMarkup(2) +
'gscheid kloan mim';
const after =
'<P>wuid Brotzeit? Pfenningguat Stubn bitt da, ' +
'hog di hi fei nia need nia need Radler gscheid kloan mim';
const diff = service.diff(before, after);
expect(diff).toBe(
'<p>' +
noMarkup(1) +
'wuid Brotzeit? Pfenningguat Stubn bitt da, ' +
'hog di hi fei nia need nia need <del>Goaßmaß </del><ins>Radler </ins>' +
brMarkup(2) +
'gscheid kloan mim</p>'
);
}));
}); });
describe('addCSSClassToFirstTag function', () => { describe('addCSSClassToFirstTag function', () => {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { LinenumberingService } from './linenumbering.service'; import { LineNumberedString, LinenumberingService } from './linenumbering.service';
import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change';
const ELEMENT_NODE = 1; const ELEMENT_NODE = 1;
@ -1318,12 +1318,12 @@ export class DiffService {
* - extracting line 2 to 3 results in <p class="os-split-after os-split-before">Line 2</p> * - extracting line 2 to 3 results in <p class="os-split-after os-split-before">Line 2</p>
* - extracting line 3 to null/4 results in <p class="os-split-before">Line 3</p> * - extracting line 3 to null/4 results in <p class="os-split-before">Line 3</p>
* *
* @param {string} htmlIn * @param {LineNumberedString} htmlIn
* @param {number} fromLine * @param {number} fromLine
* @param {number} toLine * @param {number} toLine
* @returns {ExtractedContent} * @returns {ExtractedContent}
*/ */
public extractRangeByLineNumbers(htmlIn: string, fromLine: number, toLine: number): ExtractedContent { public extractRangeByLineNumbers(htmlIn: LineNumberedString, fromLine: number, toLine: number): ExtractedContent {
if (typeof htmlIn !== 'string') { if (typeof htmlIn !== 'string') {
throw new Error('Invalid call - extractRangeByLineNumbers expects a string as first argument'); throw new Error('Invalid call - extractRangeByLineNumbers expects a string as first argument');
} }
@ -1878,15 +1878,28 @@ export class DiffService {
// Remove <del> tags that only delete line numbers // Remove <del> tags that only delete line numbers
// We need to do this before removing </del><del> as done in one of the next statements // We need to do this before removing </del><del> as done in one of the next statements
diffUnnormalized = diffUnnormalized.replace( diffUnnormalized = diffUnnormalized.replace(
/<del>((<BR CLASS="os-line-break"><\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi, /<del>(((<BR CLASS="os-line-break">)<\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
(found: string, tag: string, br: string, span: string): string => { (found: string, tag: string, brWithDel: string, plainBr: string, span: string): string => {
return (br !== undefined ? br : '') + span + ' </span>'; return (plainBr !== undefined ? plainBr : '') + span + ' </span>';
} }
); );
// Merging individual insert/delete statements into bigger blocks // Merging individual insert/delete statements into bigger blocks
diffUnnormalized = diffUnnormalized.replace(/<\/ins><ins>/gi, '').replace(/<\/del><del>/gi, ''); diffUnnormalized = diffUnnormalized.replace(/<\/ins><ins>/gi, '').replace(/<\/del><del>/gi, '');
// If we have a <del>deleted word</del>LINEBREAK<ins>new word</ins>, let's assume that the insertion
// was actually done in the same line as the deletion.
// We don't have the LINEBREAK-markers in the new string, hence we can't be a 100% sure, but
// this will probably the more frequent case.
// This only really makes a differences for change recommendations anyway, where we split the text into lines
// Hint: if there is no deletion before the line break, we have the same issue, but cannot solve this here.
diffUnnormalized = diffUnnormalized.replace(
/(<\/del>)(<BR CLASS="os-line-break"><span[^>]+os-line-number[^>]+?>\s*<\/span>)(<ins>[\s\S]*?<\/ins>)/gi,
(found: string, del: string, br: string, ins: string): string => {
return del + ins + br;
}
);
// If only a few characters of a word have changed, don't display this as a replacement of the whole word, // If only a few characters of a word have changed, don't display this as a replacement of the whole word,
// but only of these specific characters // but only of these specific characters
diffUnnormalized = diffUnnormalized.replace( diffUnnormalized = diffUnnormalized.replace(
@ -2138,7 +2151,7 @@ export class DiffService {
* @param {number} lineLength the line length * @param {number} lineLength the line length
* @return {DiffLinesInParagraph|null} * @return {DiffLinesInParagraph|null}
*/ */
public getAmendmentParagraphsLinesByMode( public getAmendmentParagraphsLines(
paragraphNo: number, paragraphNo: number,
origText: string, origText: string,
newText: string, newText: string,
@ -2190,20 +2203,18 @@ export class DiffService {
* Returns the HTML with the changes, optionally with a highlighted line. * Returns the HTML with the changes, optionally with a highlighted line.
* The original motion needs to be provided. * The original motion needs to be provided.
* *
* @param {string} motionHtml * @param {LineNumberedString} html
* @param {ViewUnifiedChange} change * @param {ViewUnifiedChange} change
* @param {number} lineLength * @param {number} lineLength
* @param {number} highlight * @param {number} highlight
* @returns {string} * @returns {string}
*/ */
public getChangeDiff( public getChangeDiff(
motionHtml: string, html: LineNumberedString,
change: ViewUnifiedChange, change: ViewUnifiedChange,
lineLength: number, lineLength: number,
highlight?: number highlight?: number
): string { ): string {
const html = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength);
let data, oldText; let data, oldText;
try { try {
@ -2248,14 +2259,14 @@ export class DiffService {
/** /**
* Returns the remainder text of the motion after the last change * Returns the remainder text of the motion after the last change
* *
* @param {string} motionHtml * @param {LineNumberedString} motionHtml
* @param {ViewUnifiedChange[]} changes * @param {ViewUnifiedChange[]} changes
* @param {number} lineLength * @param {number} lineLength
* @param {number} highlight * @param {number} highlight
* @returns {string} * @returns {string}
*/ */
public getTextRemainderAfterLastChange( public getTextRemainderAfterLastChange(
motionHtml: string, motionHtml: LineNumberedString,
changes: ViewUnifiedChange[], changes: ViewUnifiedChange[],
lineLength: number, lineLength: number,
highlight?: number highlight?: number
@ -2267,15 +2278,14 @@ export class DiffService {
} }
}, 0); }, 0);
const numberedHtml = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength, highlight);
if (changes.length === 0) { if (changes.length === 0) {
return numberedHtml; return motionHtml;
} }
let data; let data;
try { try {
data = this.extractRangeByLineNumbers(numberedHtml, maxLine, null); data = this.extractRangeByLineNumbers(motionHtml, maxLine, null);
} catch (e) { } catch (e) {
// This only happens (as far as we know) when the motion text has been altered (shortened) // This only happens (as far as we know) when the motion text has been altered (shortened)
// without modifying the change recommendations accordingly. // without modifying the change recommendations accordingly.
@ -2305,21 +2315,20 @@ export class DiffService {
/** /**
* Extracts a renderable HTML string representing the given line number range of this motion text * Extracts a renderable HTML string representing the given line number range of this motion text
* *
* @param {string} motionText * @param {LineNumberedString} motionText
* @param {LineRange} lineRange * @param {LineRange} lineRange
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string * @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
* @param {number} lineLength * @param {number} lineLength
* @param {number|null} highlightedLine * @param {number|null} highlightedLine
*/ */
public extractMotionLineRange( public extractMotionLineRange(
motionText: string, motionText: LineNumberedString,
lineRange: LineRange, lineRange: LineRange,
lineNumbers: boolean, lineNumbers: boolean,
lineLength: number, lineLength: number,
highlightedLine: number highlightedLine: number
): string { ): string {
const origHtml = this.lineNumberingService.insertLineNumbers(motionText, lineLength, highlightedLine); const extracted = this.extractRangeByLineNumbers(motionText, lineRange.from, lineRange.to);
const extracted = this.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
let html = let html =
extracted.outerContextStart + extracted.outerContextStart +
extracted.innerContextStart + extracted.innerContextStart +

View File

@ -3,6 +3,11 @@ import { Injectable } from '@angular/core';
const ELEMENT_NODE = 1; const ELEMENT_NODE = 1;
const TEXT_NODE = 3; const TEXT_NODE = 3;
/**
* A helper to indicate that certain functions expect the provided HTML strings to contain line numbers
*/
export type LineNumberedString = string;
/** /**
* Specifies a point within a HTML Text Node where a line break might be possible, if the following word * Specifies a point within a HTML Text Node where a line break might be possible, if the following word
* exceeds the maximum line length. * exceeds the maximum line length.
@ -894,7 +899,7 @@ export class LinenumberingService {
highlight?: number, highlight?: number,
callback?: () => void, callback?: () => void,
firstLine?: number firstLine?: number
): string { ): LineNumberedString {
let newHtml, newRoot; let newHtml, newRoot;
if (highlight > 0) { if (highlight > 0) {

View File

@ -199,7 +199,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* Extract the lines of the amendments * Extract the lines of the amendments
* If an amendments has multiple changes, they will be printed like an array of strings * If an amendments has multiple changes, they will be printed like an array of strings
* *
* @param amendment the motion to create the amendment to
* @return The lines of the amendment * @return The lines of the amendment
*/ */
public getChangeLines(): string { public getChangeLines(): string {

View File

@ -6,8 +6,10 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { DiffService, LineRange } from 'app/core/ui-services/diff.service'; import { DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { LineNumberedString, LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings'; import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
@ -84,7 +86,9 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param translate * @param translate
* @param matSnackBar * @param matSnackBar
* @param diff * @param diff
* @param lineNumbering
* @param recoRepo * @param recoRepo
* @param motionRepo
* @param dialogService * @param dialogService
* @param configService * @param configService
* @param el * @param el
@ -95,7 +99,9 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
protected translate: TranslateService, // protected required for ng-translate-extract protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private diff: DiffService, private diff: DiffService,
private lineNumbering: LinenumberingService,
private recoRepo: ChangeRecommendationRepositoryService, private recoRepo: ChangeRecommendationRepositoryService,
private motionRepo: MotionRepositoryService,
private dialogService: MatDialog, private dialogService: MatDialog,
private configService: ConfigService, private configService: ConfigService,
private el: ElementRef, private el: ElementRef,
@ -123,13 +129,16 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
return ''; return '';
} }
return this.diff.extractMotionLineRange( let baseText: LineNumberedString;
this.motion.text, if (this.motion.isParagraphBasedAmendment()) {
lineRange, baseText = this.motionRepo
true, .getAllAmendmentParagraphsWithOriginalLineNumbers(this.motion, this.lineLength, true)
this.lineLength, .join('\n');
this.highlightedLine } else {
); baseText = this.lineNumbering.insertLineNumbers(this.motion.text, this.lineLength);
}
return this.diff.extractMotionLineRange(baseText, lineRange, true, this.lineLength, this.highlightedLine);
} }
/** /**
@ -158,7 +167,15 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param {ViewUnifiedChange} change * @param {ViewUnifiedChange} change
*/ */
public getDiff(change: ViewUnifiedChange): string { public getDiff(change: ViewUnifiedChange): string {
return this.diff.getChangeDiff(this.motion.text, change, this.lineLength, this.highlightedLine); let motionHtml: string;
if (this.motion.isParagraphBasedAmendment()) {
const parentMotion = this.motionRepo.getViewModel(this.motion.parent_id);
motionHtml = parentMotion.text;
} else {
motionHtml = this.motion.text;
}
const baseHtml = this.lineNumbering.insertLineNumbers(motionHtml, this.lineLength);
return this.diff.getChangeDiff(baseHtml, change, this.lineLength, this.highlightedLine);
} }
/** /**
@ -168,12 +185,15 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
if (!this.lineLength) { if (!this.lineLength) {
return ''; // @TODO This happens in the test case when the lineLength-variable is not set return ''; // @TODO This happens in the test case when the lineLength-variable is not set
} }
return this.diff.getTextRemainderAfterLastChange( let baseText: LineNumberedString;
this.motion.text, if (this.motion.isParagraphBasedAmendment()) {
this.changes, baseText = this.motionRepo
this.lineLength, .getAllAmendmentParagraphsWithOriginalLineNumbers(this.motion, this.lineLength, true)
this.highlightedLine .join('\n');
); } else {
baseText = this.lineNumbering.insertLineNumbers(this.motion.text, this.lineLength);
}
return this.diff.getTextRemainderAfterLastChange(baseText, this.changes, this.lineLength, this.highlightedLine);
} }
/** /**

View File

@ -1,4 +1,5 @@
import { import {
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
@ -15,16 +16,23 @@ import { LineRange, ModificationType } from 'app/core/ui-services/diff.service';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation'; import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
/** /**
* This component displays the original motion text with annotated change commendations * This component displays either the original motion text or the original amendment diff text
* and a method to create new change recommendations from the line numbers to the left of the text. * with annotated change commendations and a method to create new change recommendations
* It's called from motion-details for displaying the whole motion text as well as from the diff view to show the * from the line numbers to the left of the text.
* unchanged parts of the motion. * It's called from motion-details for displaying the whole motion text as well as from the
* motion's or amendment's diff view to show the unchanged parts of the motion.
* *
* The line numbers are provided within the pre-rendered HTML, so we have to work with raw HTML * The line numbers are provided within the pre-rendered HTML, so we have to work with raw HTML
* and native HTML elements. * and native HTML elements.
* *
* It takes the styling from the parent component. * It takes the styling from the parent component.
* *
* Special hints regarding amendments:
* When used for paragraph-based amendments, this component is embedded once for each paragraph. Hence,
* not all changeRecommendations provided are relevant for this paragraph (as we put the decision about
* which changeRecommendations are relevant in this component, not the caller).
* TODO: Right now, only change recommendations affecting only one paragraph are supported
*
* ## Examples * ## Examples
* *
* ```html * ```html
@ -63,12 +71,25 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
public can_manage = false; public can_manage = false;
// Calculated from the embedded line numbers after the text has been set.
// Hint: this numbering refers to the actual lines, not the line number markers;
// hence, if maxLineNo === 10, line no. 10 is still visible.
// This is semantically different from the diff algorithms.
private minLineNo: number = null;
private maxLineNo: number = null;
/** /**
* @param {Renderer2} renderer * @param {Renderer2} renderer
* @param {ElementRef} el * @param {ElementRef} el
* @param {ChangeDetectorRef} cd
* @param {OperatorService} operator * @param {OperatorService} operator
*/ */
public constructor(private renderer: Renderer2, private el: ElementRef, private operator: OperatorService) { public constructor(
private renderer: Renderer2,
private el: ElementRef,
private cd: ChangeDetectorRef,
private operator: OperatorService
) {
this.operator.getUserObservable().subscribe(this.onPermissionsChanged.bind(this)); this.operator.getUserObservable().subscribe(this.onPermissionsChanged.bind(this));
} }
@ -107,7 +128,21 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
} }
public getTextChangeRecommendations(): ViewMotionChangeRecommendation[] { public getTextChangeRecommendations(): ViewMotionChangeRecommendation[] {
return this.changeRecommendations.filter(reco => !reco.isTitleChange()); return this.changeRecommendations
.filter(reco => !reco.isTitleChange())
.filter(reco => reco.line_from >= this.minLineNo && reco.line_from <= this.maxLineNo);
}
private setLineNumberCache(): void {
Array.from(this.element.querySelectorAll('.os-line-number')).forEach((lineNumberEl: Element) => {
const lineNumber = parseInt(lineNumberEl.getAttribute('data-line-number'), 10);
if (this.minLineNo === null || lineNumber < this.minLineNo) {
this.minLineNo = lineNumber;
}
if (this.maxLineNo === null || lineNumber > this.maxLineNo) {
this.maxLineNo = lineNumber;
}
});
} }
/** /**
@ -282,7 +317,9 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
// If we show it right away, there will be nasty Angular warnings about changed values, as the position // If we show it right away, there will be nasty Angular warnings about changed values, as the position
// is changing while the DOM updates // is changing while the DOM updates
window.setTimeout(() => { window.setTimeout(() => {
this.setLineNumberCache();
this.showChangeRecommendations = true; this.showChangeRecommendations = true;
this.cd.detectChanges();
}, 1); }, 1);
} }

View File

@ -530,7 +530,6 @@
[matMenuTriggerFor]="changeRecoMenu" [matMenuTriggerFor]="changeRecoMenu"
*ngIf=" *ngIf="
motion && motion &&
!motion.isParagraphBasedAmendment() &&
((allChangingObjects && allChangingObjects.length) || motion.modified_final_version) ((allChangingObjects && allChangingObjects.length) || motion.modified_final_version)
" "
> >
@ -874,33 +873,40 @@
<div class="alert alert-info" *ngIf="motion.diffLines.length === 0"> <div class="alert alert-info" *ngIf="motion.diffLines.length === 0">
<span>{{ 'No changes at the text.' | translate }}</span> <span>{{ 'No changes at the text.' | translate }}</span>
</div> </div>
<div <ng-container *ngIf="!isRecoMode(ChangeRecoMode.Diff) && !isFinalEdit">
*ngFor="let paragraph of getAmendmentParagraphs(showAmendmentContext)" <div
class="motion-text motion-text-diff amendment-view" *ngFor="let paragraph of getAmendmentParagraphs()"
[class.line-numbers-none]="isLineNumberingNone()" class="motion-text motion-text-diff amendment-view"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-outside]="isLineNumberingOutside()" [class.line-numbers-inline]="isLineNumberingInline()"
[class.amendment-context]="showAmendmentContext" [class.line-numbers-outside]="isLineNumberingOutside()"
> [class.amendment-context]="showAmendmentContext"
<!-- TODO: everything here is required for PDF as well. Should be in a service -->
<h3
*ngIf="paragraph.diffLineTo === paragraph.diffLineFrom + 1 && !showAmendmentContext"
class="amendment-line-header"
> >
{{ 'Line' | translate }} {{ paragraph.diffLineFrom }}: <h3 class="amendment-line-header" *ngIf="!showAmendmentContext">{{ getAmendmentParagraphLinesTitle(paragraph) }}</h3>
</h3>
<h3
*ngIf="paragraph.diffLineTo !== paragraph.diffLineFrom + 1 && !showAmendmentContext"
class="amendment-line-header"
>
{{ 'Line' | translate }} {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}:
</h3>
<!-- TODO: Seems to be directly duplicated in the slide --> <os-motion-detail-original-change-recommendations
<div class="paragraphcontext" [innerHtml]="paragraph.textPre | trust: 'html'"></div> *ngIf="isLineNumberingOutside() && (isRecoMode(ChangeRecoMode.Original) || isRecoMode(ChangeRecoMode.Changed))"
<div [innerHtml]="paragraph.text | trust: 'html'"></div> [html]="getAmendmentDiffTextWithContext(paragraph)"
<div class="paragraphcontext" [innerHtml]="paragraph.textPost | trust: 'html'"></div> [changeRecommendations]="changeRecommendations"
</div> (createChangeRecommendation)="createChangeRecommendation($event)"
(gotoChangeRecommendation)="gotoChangeRecommendation($event)"
></os-motion-detail-original-change-recommendations>
<div
*ngIf="!isLineNumberingOutside() || !(isRecoMode(ChangeRecoMode.Original) || isRecoMode(ChangeRecoMode.Changed))"
[outerHTML]="getAmendmentDiffTextWithContext(paragraph) | trust: 'html'"
></div><!-- the <div> element is only a placeholder -> outerHTML to replace it -->
</div>
</ng-container>
<os-motion-detail-diff
*ngIf="isRecoMode(ChangeRecoMode.Diff)"
[motion]="motion"
[changes]="getChangesForDiffMode()"
[scrollToChange]="scrollToChange"
[highlightedLine]="highlightedLine"
[lineNumberingMode]="lnMode"
(createChangeRecommendation)="createChangeRecommendation($event)"
></os-motion-detail-diff>
</div> </div>
<div *ngIf="!motion.diffLines"> <div *ngIf="!motion.diffLines">
<span class="red-warning-text">{{ <span class="red-warning-text">{{
@ -910,7 +916,7 @@
</section> </section>
<!-- Show entire motion text --> <!-- Show entire motion text -->
<div> <div *ngIf="isRecoMode(ChangeRecoMode.Original) || isRecoMode(ChangeRecoMode.Changed)">
<mat-checkbox <mat-checkbox
(change)="showAmendmentContext = !showAmendmentContext" (change)="showAmendmentContext = !showAmendmentContext"
*ngIf="motion && motion.isParagraphBasedAmendment()" *ngIf="motion && motion.isParagraphBasedAmendment()"
@ -953,7 +959,10 @@
</div> </div>
</mat-menu> </mat-menu>
<!-- Diff View Menu --> <!-- Diff View Menu
For motions, all items are available if there are changing objects. The final print template only after is has been created.
For paragraph-based amendments, only the original and the diff version is available.
-->
<mat-menu #changeRecoMenu="matMenu"> <mat-menu #changeRecoMenu="matMenu">
<button <button
mat-menu-item mat-menu-item
@ -982,13 +991,13 @@
mat-menu-item mat-menu-item
(click)="setChangeRecoMode(ChangeRecoMode.Final)" (click)="setChangeRecoMode(ChangeRecoMode.Final)"
[ngClass]="{ selected: crMode === ChangeRecoMode.Final }" [ngClass]="{ selected: crMode === ChangeRecoMode.Final }"
*ngIf="allChangingObjects && allChangingObjects.length" *ngIf="motion && !motion.isParagraphBasedAmendment() && allChangingObjects && allChangingObjects.length"
> >
{{ 'Final version' | translate }} {{ 'Final version' | translate }}
</button> </button>
<button <button
mat-menu-item mat-menu-item
*ngIf="motion && motion.modified_final_version" *ngIf="motion && motion.modified_final_version && !motion.isParagraphBasedAmendment()"
(click)="setChangeRecoMode(ChangeRecoMode.ModifiedFinal)" (click)="setChangeRecoMode(ChangeRecoMode.ModifiedFinal)"
[ngClass]="{ selected: crMode === ChangeRecoMode.ModifiedFinal }" [ngClass]="{ selected: crMode === ChangeRecoMode.ModifiedFinal }"
> >

View File

@ -215,10 +215,21 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
public changeRecommendations: ViewMotionChangeRecommendation[]; public changeRecommendations: ViewMotionChangeRecommendation[];
/** /**
* All amendments to this motions * All amendments to this motion
*/ */
public amendments: ViewMotion[]; public amendments: ViewMotion[];
/**
* The change recommendations to amendments to this motion
*/
public amendmentChangeRecos: { [amendmentId: string]: ViewMotionChangeRecommendation[] } = {};
/**
* The observables for the `amendmentChangeRecos` field above.
* Necessary to track which amendments' change recommendations we have already subscribed to.
*/
public amendmentChangeRecoSubscriptions: { [amendmentId: string]: Subscription } = {};
/** /**
* All change recommendations AND amendments, sorted by line number. * All change recommendations AND amendments, sorted by line number.
*/ */
@ -416,25 +427,25 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* @param repo Motion Repository * @param repo Motion Repository
* @param changeRecoRepo Change Recommendation Repository * @param changeRecoRepo Change Recommendation Repository
* @param statuteRepo: Statute Paragraph Repository * @param statuteRepo: Statute Paragraph Repository
* @param mediafileRepo Mediafile Repository
* @param DS The DataStoreService
* @param configService The configuration provider * @param configService The configuration provider
* @param promptService ensure safe deletion * @param promptService ensure safe deletion
* @param pdfExport export the motion to pdf * @param pdfExport export the motion to pdf
* @param personalNoteService: personal comments and favorite marker * @param personalNoteService: personal comments and favorite marker
* @param linenumberingService The line numbering service * @param linenumberingService The line numbering service
* @param categoryRepo Repository for categories * @param categoryRepo Repository for categories
* @param viewModelStore accessing view models
* @param categoryRepo access the category repository
* @param userRepo Repository for users * @param userRepo Repository for users
* @param notifyService: NotifyService work with notification * @param notifyService: NotifyService work with notification
* @param tagRepo * @param tagRepo
* @param mediaFilerepo
* @param workflowRepo * @param workflowRepo
* @param blockRepo * @param blockRepo
* @param itemRepo * @param itemRepo
* @param motionSortService * @param motionSortService
* @param motionFilterListService * @param amendmentSortService
* @param motionFilterService
* @param amendmentFilterService
* @param cd ChangeDetectorRef
* @param pollDialog
* @param motionPollService
*/ */
public constructor( public constructor(
title: Title, title: Title,
@ -590,6 +601,24 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
}); });
} }
/**
* Subscribes to all new amendment's change recommendations so we can access their data for the diff view
*/
private resetAmendmentChangeRecoListener(): void {
this.amendments.forEach((amendment: ViewMotion) => {
if (this.amendmentChangeRecoSubscriptions[amendment.id] === undefined) {
this.amendmentChangeRecoSubscriptions[
amendment.id
] = this.changeRecoRepo
.getChangeRecosOfMotionObservable(amendment.id)
.subscribe((changeRecos: ViewMotionChangeRecommendation[]): void => {
this.amendmentChangeRecos[amendment.id] = changeRecos;
this.recalcUnifiedChanges();
});
}
});
}
/** /**
* Merges amendments and change recommendations and sorts them by the line numbers. * Merges amendments and change recommendations and sorts them by the line numbers.
* Called each time one of these arrays changes. * Called each time one of these arrays changes.
@ -614,8 +643,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} }
if (this.amendments) { if (this.amendments) {
this.amendments.forEach((amendment: ViewMotion): void => { this.amendments.forEach((amendment: ViewMotion): void => {
const toApplyChanges = (this.amendmentChangeRecos[amendment.id] || []).filter(
// The rejected change recommendations for amendments should not be considered
change => change.showInFinalView()
);
this.repo this.repo
.getAmendmentAmendedParagraphs(amendment, this.lineLength) .getAmendmentAmendedParagraphs(amendment, this.lineLength, toApplyChanges)
.forEach((change: ViewUnifiedChange): void => { .forEach((change: ViewUnifiedChange): void => {
this.allChangingObjects.push(change); this.allChangingObjects.push(change);
}); });
@ -630,6 +663,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
return 0; return 0;
} }
}); });
// When "diff" is the default view mode, the crMode might have been set
// before the motion and allChangingObjects have been loaded. As the availability of "diff" depends on
// allChangingObjects, we set "diff" first in this case (in the config-listener) and perform the actual
// check if "diff" is possible now.
// Test: "diff" as default view. Open a motion, create an amendment. "Original" should be set automatically.
this.crMode = this.determineCrMode(this.crMode);
this.cd.markForCheck(); this.cd.markForCheck();
} }
@ -664,6 +705,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
this.repo.amendmentsTo(motionId).subscribe((amendments: ViewMotion[]): void => { this.repo.amendmentsTo(motionId).subscribe((amendments: ViewMotion[]): void => {
this.amendments = amendments; this.amendments = amendments;
this.resetAmendmentChangeRecoListener();
this.recalcUnifiedChanges(); this.recalcUnifiedChanges();
}), }),
this.repo this.repo
@ -908,15 +950,48 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
return formatedText; return formatedText;
} }
/**
* This returns the plain HTML of a changed area in an amendment, including its context,
* for the purpose of piping it into <motion-detail-original-change-recommendations>.
* This component works with plain HTML, hence we are composing plain HTML here, too.
*
* @param {DiffLinesInParagraph} paragraph
* @returns {string}
*
* TODO: Seems to be directly duplicated in the slide
*/
public getAmendmentDiffTextWithContext(paragraph: DiffLinesInParagraph): string {
return (
'<div class="paragraphcontext">' +
paragraph.textPre +
'</div>' +
'<div>' +
paragraph.text +
'</div>' +
'<div class="paragraphcontext">' +
paragraph.textPost +
'</div>'
);
}
/** /**
* If `this.motion` is an amendment, this returns the list of all changed paragraphs. * If `this.motion` is an amendment, this returns the list of all changed paragraphs.
* TODO: Cleanup: repo function could be injected part of the model, to have easier access * TODO: Cleanup: repo function could be injected part of the model, to have easier access
* *
* @param {boolean} includeUnchanged
* @returns {DiffLinesInParagraph[]} * @returns {DiffLinesInParagraph[]}
*/ */
public getAmendmentParagraphs(includeUnchanged: boolean): DiffLinesInParagraph[] { public getAmendmentParagraphs(): DiffLinesInParagraph[] {
return this.repo.getAmendmentParagraphs(this.motion, this.lineLength, includeUnchanged); return this.repo.getAmendmentParagraphLines(
this.motion,
this.lineLength,
this.crMode,
this.changeRecommendations,
this.showAmendmentContext
);
}
public getAmendmentParagraphLinesTitle(paragraph: DiffLinesInParagraph): string {
return this.repo.getAmendmentParagraphLinesTitle(paragraph);
} }
/** /**
@ -1063,12 +1138,27 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
editChangeRecommendation: false, editChangeRecommendation: false,
newChangeRecommendation: true, newChangeRecommendation: true,
lineRange: lineRange, lineRange: lineRange,
changeRecommendation: this.changeRecoRepo.createChangeRecommendationTemplate( changeRecommendation: null
};
if (this.motion.isParagraphBasedAmendment()) {
const lineNumberedParagraphs = this.repo.getAllAmendmentParagraphsWithOriginalLineNumbers(
this.motion,
this.lineLength,
false
);
data.changeRecommendation = this.changeRecoRepo.createAmendmentChangeRecommendationTemplate(
this.motion,
lineNumberedParagraphs,
lineRange,
this.lineLength
);
} else {
data.changeRecommendation = this.changeRecoRepo.createMotionChangeRecommendationTemplate(
this.motion, this.motion,
lineRange, lineRange,
this.lineLength this.lineLength
) );
}; }
this.dialogService.open(MotionChangeRecommendationDialogComponent, { this.dialogService.open(MotionChangeRecommendationDialogComponent, {
...mediumDialogSettings, ...mediumDialogSettings,
data: data data: data
@ -1329,7 +1419,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
/** /**
* Adds or removes a tag to the current motion * Adds or removes a tag to the current motion
* *
* @param id Motion tag id * @param {MouseEvent} event
* @param {number} id Motion tag id
*/ */
public setTag(event: MouseEvent, id: number): void { public setTag(event: MouseEvent, id: number): void {
event.stopPropagation(); event.stopPropagation();
@ -1497,7 +1588,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* Function to handle leaving persons and * Function to handle leaving persons and
* recognize if there is no other person editing the same motion anymore. * recognize if there is no other person editing the same motion anymore.
* *
* @param senderId The id of the sender who has left the editing-view. * @param senderName The name of the sender who has left the editing-view.
*/ */
private recognizeOtherWorkerOnMotion(senderName: string): void { private recognizeOtherWorkerOnMotion(senderName: string): void {
this.otherWorkOnMotion = this.otherWorkOnMotion.filter(value => value !== senderName); this.otherWorkOnMotion = this.otherWorkOnMotion.filter(value => value !== senderName);

View File

@ -64,7 +64,14 @@ export class MotionCsvExportService {
const amendments = this.motionRepo.getAmendmentsInstantly(motion.id); const amendments = this.motionRepo.getAmendmentsInstantly(motion.id);
if (amendments) { if (amendments) {
for (const amendment of amendments) { for (const amendment of amendments) {
const changedParagraphs = this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength); const changeRecos = this.changeRecoRepo
.getChangeRecoOfMotion(amendment.id)
.filter(reco => reco.showInFinalView());
const changedParagraphs = this.motionRepo.getAmendmentAmendedParagraphs(
amendment,
lineLength,
changeRecos
);
for (const change of changedParagraphs) { for (const change of changedParagraphs) {
changes.push(change as ViewUnifiedChange); changes.push(change as ViewUnifiedChange);
} }

View File

@ -576,17 +576,16 @@ export class MotionPdfService {
if (motion.isParagraphBasedAmendment()) { if (motion.isParagraphBasedAmendment()) {
// this is logically redundant with the formation of amendments in the motion-detail html. // this is logically redundant with the formation of amendments in the motion-detail html.
// Should be refactored in a way that a service returns the correct html for both cases // Should be refactored in a way that a service returns the correct html for both cases
for (const paragraph of this.motionRepo.getAmendmentParagraphs(motion, lineLength, false)) { const changeRecos = this.changeRecoRepo.getChangeRecoOfMotion(motion.id);
if (paragraph.diffLineTo === paragraph.diffLineFrom + 1) { const amendmentParas = this.motionRepo.getAmendmentParagraphLines(
motionText += `<h3> motion,
${this.translate.instant('Line')} ${paragraph.diffLineFrom}: lineLength,
</h3>`; crMode,
} else { changeRecos,
motionText += `<h3> false
${this.translate.instant('Line')} ${paragraph.diffLineFrom} - ${paragraph.diffLineTo - 1}: );
</h3>`; for (const paragraph of amendmentParas) {
} motionText += '<h3>' + this.motionRepo.getAmendmentParagraphLinesTitle(paragraph) + '</h3>';
motionText += `<div class="paragraphcontext">${paragraph.textPre}</div>`; motionText += `<div class="paragraphcontext">${paragraph.textPre}</div>`;
motionText += paragraph.text; motionText += paragraph.text;
motionText += `<div class="paragraphcontext">${paragraph.textPost}</div>`; motionText += `<div class="paragraphcontext">${paragraph.textPost}</div>`;
@ -634,11 +633,12 @@ export class MotionPdfService {
return this.changeRecoRepo return this.changeRecoRepo
.getChangeRecoOfMotion(motion.id) .getChangeRecoOfMotion(motion.id)
.concat( .concat(
this.motionRepo this.motionRepo.getAmendmentsInstantly(motion.id).flatMap((amendment: ViewMotion) => {
.getAmendmentsInstantly(motion.id) const changeRecos = this.changeRecoRepo
.flatMap((amendment: ViewMotion) => .getChangeRecoOfMotion(amendment.id)
this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength) .filter(reco => reco.showInFinalView());
) return this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength, changeRecos);
})
) )
.sort((a, b) => a.getLineFrom() - b.getLineFrom()) as ViewUnifiedChange[]; .sort((a, b) => a.getLineFrom() - b.getLineFrom()) as ViewUnifiedChange[];
} }

View File

@ -6,7 +6,7 @@ import { SlideData } from 'app/core/core-services/projector-data.service';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { DiffLinesInParagraph, DiffService, LineRange } from 'app/core/ui-services/diff.service'; import { DiffLinesInParagraph, DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; import { LineNumberedString, LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { MotionTitleInformation } from 'app/site/motions/models/view-motion'; import { MotionTitleInformation } from 'app/site/motions/models/view-motion';
import { ChangeRecoMode, LineNumberingMode } from 'app/site/motions/motions.constants'; import { ChangeRecoMode, LineNumberingMode } from 'app/site/motions/motions.constants';
@ -270,19 +270,18 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
/** /**
* Extracts a renderable HTML string representing the given line number range of this motion * Extracts a renderable HTML string representing the given line number range of this motion
* *
* @param {string} motionHtml * @param {LineNumberedString} motionHtml
* @param {LineRange} lineRange * @param {LineRange} lineRange
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string * @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
* @param {number} lineLength * @param {number} lineLength
*/ */
public extractMotionLineRange( public extractMotionLineRange(
motionHtml: string, motionHtml: LineNumberedString,
lineRange: LineRange, lineRange: LineRange,
lineNumbers: boolean, lineNumbers: boolean,
lineLength: number lineLength: number
): string { ): string {
const origHtml = this.lineNumbering.insertLineNumbers(motionHtml, this.lineLength, this.highlightedLine); const extracted = this.diff.extractRangeByLineNumbers(motionHtml, lineRange.from, lineRange.to);
const extracted = this.diff.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
let html = let html =
extracted.outerContextStart + extracted.outerContextStart +
extracted.innerContextStart + extracted.innerContextStart +
@ -343,21 +342,22 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
const changes = this.getAllTextChangingObjects().filter(change => { const changes = this.getAllTextChangingObjects().filter(change => {
return change.showInDiffView(); return change.showInDiffView();
}); });
const motionText = this.lineNumbering.insertLineNumbers(motion.text, this.lineLength);
changes.forEach((change: ViewUnifiedChange, idx: number) => { changes.forEach((change: ViewUnifiedChange, idx: number) => {
if (idx === 0) { if (idx === 0) {
const lineRange = { from: 1, to: change.getLineFrom() }; const lineRange = { from: 1, to: change.getLineFrom() };
text += this.extractMotionLineRange(motion.text, lineRange, true, this.lineLength); text += this.extractMotionLineRange(motionText, lineRange, true, this.lineLength);
} else if (changes[idx - 1].getLineTo() < change.getLineFrom()) { } else if (changes[idx - 1].getLineTo() < change.getLineFrom()) {
const lineRange = { const lineRange = {
from: changes[idx - 1].getLineTo(), from: changes[idx - 1].getLineTo(),
to: change.getLineFrom() to: change.getLineFrom()
}; };
text += this.extractMotionLineRange(motion.text, lineRange, true, this.lineLength); text += this.extractMotionLineRange(motionText, lineRange, true, this.lineLength);
} }
text += this.diff.getChangeDiff(motion.text, change, this.lineLength, this.highlightedLine); text += this.diff.getChangeDiff(motionText, change, this.lineLength, this.highlightedLine);
}); });
text += this.diff.getTextRemainderAfterLastChange( text += this.diff.getTextRemainderAfterLastChange(
motion.text, motionText,
changes, changes,
this.lineLength, this.lineLength,
this.highlightedLine this.highlightedLine
@ -412,7 +412,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
return null; return null;
} }
// Hint: can be either DiffLinesInParagraph or null, if no changes are made // Hint: can be either DiffLinesInParagraph or null, if no changes are made
return this.diff.getAmendmentParagraphsLinesByMode( return this.diff.getAmendmentParagraphsLines(
paraNo, paraNo,
baseParagraphs[paraNo], baseParagraphs[paraNo],
newText, newText,