Merge pull request #4246 from CatoTH/OS3-Slides-LineNumbering-Diff
Initial support for line numbering and CR in Projector
This commit is contained in:
commit
69539cacbc
@ -27,7 +27,7 @@ import { TreeService } from 'app/core/ui-services/tree.service';
|
||||
import { User } from 'app/shared/models/users/user';
|
||||
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-change-recommendation';
|
||||
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
|
||||
import { ViewUnifiedChange } from 'app/site/motions/models/view-unified-change';
|
||||
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
|
||||
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
|
||||
import { Workflow } from 'app/shared/models/motions/workflow';
|
||||
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
|
||||
@ -397,7 +397,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
case ChangeRecoMode.Original:
|
||||
return this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength, highlightLine);
|
||||
case ChangeRecoMode.Changed:
|
||||
return this.diff.getTextWithChanges(targetMotion, changes, lineLength, highlightLine);
|
||||
return this.diff.getTextWithChanges(targetMotion.text, changes, lineLength, highlightLine);
|
||||
case ChangeRecoMode.Diff:
|
||||
let text = '';
|
||||
changes.forEach((change: ViewUnifiedChange, idx: number) => {
|
||||
@ -424,13 +424,18 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
highlightLine
|
||||
);
|
||||
}
|
||||
text += this.getChangeDiff(targetMotion, change, lineLength, highlightLine);
|
||||
text += this.diff.getChangeDiff(targetMotion.text, change, lineLength, highlightLine);
|
||||
});
|
||||
text += this.getTextRemainderAfterLastChange(targetMotion, changes, lineLength, highlightLine);
|
||||
text += this.diff.getTextRemainderAfterLastChange(
|
||||
targetMotion.text,
|
||||
changes,
|
||||
lineLength,
|
||||
highlightLine
|
||||
);
|
||||
return text;
|
||||
case ChangeRecoMode.Final:
|
||||
const appliedChanges: ViewUnifiedChange[] = changes.filter(change => change.isAccepted());
|
||||
return this.diff.getTextWithChanges(targetMotion, appliedChanges, lineLength, highlightLine);
|
||||
return this.diff.getTextWithChanges(targetMotion.text, appliedChanges, lineLength, highlightLine);
|
||||
case ChangeRecoMode.ModifiedFinal:
|
||||
if (targetMotion.modified_final_version) {
|
||||
return this.lineNumbering.insertLineNumbers(
|
||||
@ -496,63 +501,6 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder text of the motion after the last change
|
||||
*
|
||||
* @param {ViewMotion} motion
|
||||
* @param {ViewUnifiedChange[]} changes
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getTextRemainderAfterLastChange(
|
||||
motion: ViewMotion,
|
||||
changes: ViewUnifiedChange[],
|
||||
lineLength: number,
|
||||
highlight?: number
|
||||
): string {
|
||||
let maxLine = 1;
|
||||
changes.forEach((change: ViewUnifiedChange) => {
|
||||
if (change.getLineTo() > maxLine) {
|
||||
maxLine = change.getLineTo();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
const numberedHtml = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
||||
if (changes.length === 0) {
|
||||
return numberedHtml;
|
||||
}
|
||||
|
||||
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;
|
||||
html = this.lineNumbering.insertLineNumbers(html, lineLength, highlight, null, maxLine);
|
||||
} else {
|
||||
// Prevents empty lines at the end of the motion
|
||||
html = '';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last line number of a motion
|
||||
*
|
||||
@ -567,7 +515,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ViewChangeReco} object based on the motion ID and the given lange range.
|
||||
* Creates a {@link ViewMotionChangeRecommendation} 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
|
||||
@ -590,66 +538,6 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
return new ViewMotionChangeRecommendation(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
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getChangeDiff(
|
||||
motion: ViewMotion,
|
||||
change: ViewUnifiedChange,
|
||||
lineLength: number,
|
||||
highlight?: number
|
||||
): string {
|
||||
const html = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an amendment, this returns the motion affected by this amendments
|
||||
*
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { LinenumberingService } from './linenumbering.service';
|
||||
import { ViewMotion } from '../../site/motions/models/view-motion';
|
||||
import { ViewUnifiedChange } from '../../site/motions/models/view-unified-change';
|
||||
import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change';
|
||||
|
||||
const ELEMENT_NODE = 1;
|
||||
const TEXT_NODE = 3;
|
||||
@ -2036,18 +2035,18 @@ export class DiffService {
|
||||
/**
|
||||
* Applies all given changes to the motion and returns the (line-numbered) text
|
||||
*
|
||||
* @param {ViewMotion} motion
|
||||
* @param {string} motionHtml
|
||||
* @param {ViewUnifiedChange[]} changes
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlightLine
|
||||
*/
|
||||
public getTextWithChanges(
|
||||
motion: ViewMotion,
|
||||
motionHtml: string,
|
||||
changes: ViewUnifiedChange[],
|
||||
lineLength: number,
|
||||
highlightLine: number
|
||||
): string {
|
||||
let html = motion.text;
|
||||
let html = motionHtml;
|
||||
|
||||
// Changes need to be applied from the bottom up, to prevent conflicts with changing line numbers.
|
||||
changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => {
|
||||
@ -2127,4 +2126,120 @@ export class DiffService {
|
||||
textPost: textPost
|
||||
} as DiffLinesInParagraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML with the changes, optionally with a highlighted line.
|
||||
* The original motion needs to be provided.
|
||||
*
|
||||
* @param {string} motionHtml
|
||||
* @param {ViewUnifiedChange} change
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getChangeDiff(
|
||||
motionHtml: string,
|
||||
change: ViewUnifiedChange,
|
||||
lineLength: number,
|
||||
highlight?: number
|
||||
): string {
|
||||
const html = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength);
|
||||
|
||||
let data, oldText;
|
||||
|
||||
try {
|
||||
data = this.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.lineNumberingService.insertLineNumbers(oldText, lineLength, null, null, change.getLineFrom());
|
||||
let diff = this.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.lineNumberingService.insertLineBreaksWithoutNumbers(diff, lineLength, true);
|
||||
|
||||
if (highlight > 0) {
|
||||
diff = this.lineNumberingService.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.addCSSClassToFirstTag(origBeginning, 'merge-before') + diff.substring(origBeginning.length);
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder text of the motion after the last change
|
||||
*
|
||||
* @param {string} motionHtml
|
||||
* @param {ViewUnifiedChange[]} changes
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getTextRemainderAfterLastChange(
|
||||
motionHtml: string,
|
||||
changes: ViewUnifiedChange[],
|
||||
lineLength: number,
|
||||
highlight?: number
|
||||
): string {
|
||||
let maxLine = 1;
|
||||
changes.forEach((change: ViewUnifiedChange) => {
|
||||
if (change.getLineTo() > maxLine) {
|
||||
maxLine = change.getLineTo();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
const numberedHtml = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength);
|
||||
if (changes.length === 0) {
|
||||
return numberedHtml;
|
||||
}
|
||||
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = this.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.addCSSClassToFirstTag(data.outerContextStart + data.innerContextStart, 'merge-before') +
|
||||
data.html +
|
||||
data.innerContextEnd +
|
||||
data.outerContextEnd;
|
||||
html = this.lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, maxLine);
|
||||
} else {
|
||||
// Prevents empty lines at the end of the motion
|
||||
html = '';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Component } from '@angular/core';
|
||||
import { LineNumberingMode, ViewMotion } from '../../models/view-motion';
|
||||
import { MotionDetailDiffComponent } from './motion-detail-diff.component';
|
||||
import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
||||
import { ViewUnifiedChange } from '../../models/view-unified-change';
|
||||
import { ViewUnifiedChange } from '../../../../shared/models/motions/view-unified-change';
|
||||
import { Motion } from 'app/shared/models/motions/motion';
|
||||
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
|
||||
|
||||
|
@ -5,9 +5,9 @@ import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { LineNumberingMode, ViewMotion } from '../../models/view-motion';
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../models/view-unified-change';
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../../shared/models/motions/view-unified-change';
|
||||
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
||||
import { LineRange, ModificationType } from 'app/core/ui-services/diff.service';
|
||||
import { DiffService, LineRange, ModificationType } from 'app/core/ui-services/diff.service';
|
||||
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
|
||||
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
|
||||
import {
|
||||
@ -69,6 +69,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
* @param matSnackBar
|
||||
* @param sanitizer
|
||||
* @param motionRepo
|
||||
* @param diff
|
||||
* @param recoRepo
|
||||
* @param dialogService
|
||||
* @param configService
|
||||
@ -80,6 +81,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
matSnackBar: MatSnackBar,
|
||||
private sanitizer: DomSanitizer,
|
||||
private motionRepo: MotionRepositoryService,
|
||||
private diff: DiffService,
|
||||
private recoRepo: ChangeRecommendationRepositoryService,
|
||||
private dialogService: MatDialog,
|
||||
private configService: ConfigService,
|
||||
@ -142,7 +144,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
* @param {ViewUnifiedChange} change
|
||||
*/
|
||||
public getDiff(change: ViewUnifiedChange): SafeHtml {
|
||||
const html = this.motionRepo.getChangeDiff(this.motion, change, this.lineLength, this.highlightedLine);
|
||||
const html = this.diff.getChangeDiff(this.motion.text, change, this.lineLength, this.highlightedLine);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||
}
|
||||
|
||||
@ -153,8 +155,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
if (!this.lineLength) {
|
||||
return ''; // @TODO This happens in the test case when the lineLength-variable is not set
|
||||
}
|
||||
return this.motionRepo.getTextRemainderAfterLastChange(
|
||||
this.motion,
|
||||
return this.diff.getTextRemainderAfterLastChange(
|
||||
this.motion.text,
|
||||
this.changes,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import '../../../../../assets/styles/motion-styles-common';
|
||||
|
||||
span {
|
||||
margin: 0;
|
||||
}
|
||||
@ -150,138 +152,6 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
/* Line numbers */
|
||||
// :host ::ng-deep is needed as this styling applies to the motion html that is injected using innerHTML,
|
||||
// which doesn't have the [ngcontent]-attributes necessary for regular styles.
|
||||
// An alternative approach (in case ::ng-deep gets removed) might be to change the view encapsulation.
|
||||
:host ::ng-deep .motion-text {
|
||||
ins,
|
||||
.insert {
|
||||
color: green;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
del,
|
||||
.delete {
|
||||
color: red;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: #ff0;
|
||||
}
|
||||
&.line-numbers-outside {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
.os-line-number {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
padding-right: 55px;
|
||||
|
||||
&:after {
|
||||
content: attr(data-line-number);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
vertical-align: top;
|
||||
color: gray;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&.selectable:hover:before,
|
||||
&.selected:before {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 20px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" fill="%23337ab7"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.line-numbers-inline {
|
||||
.os-line-break {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.os-line-number {
|
||||
display: inline-block;
|
||||
|
||||
&:after {
|
||||
display: inline-block;
|
||||
content: attr(data-line-number);
|
||||
vertical-align: top;
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
color: gray;
|
||||
margin-top: -3px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.line-numbers-none {
|
||||
.os-line-break {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.os-line-number {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.os-split-before {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.os-split-after {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
li.os-split-before {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .amendment-view {
|
||||
.os-split-after {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.os-split-before {
|
||||
margin-top: 0;
|
||||
}
|
||||
.paragraphcontext {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.amendment-context .paragraphcontext {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.main-nav-color {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions
|
||||
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
|
||||
import { ViewCreateMotion } from '../../models/view-create-motion';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ViewUnifiedChange } from '../../models/view-unified-change';
|
||||
import { ViewUnifiedChange } from '../../../../shared/models/motions/view-unified-change';
|
||||
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
|
||||
import { Workflow } from 'app/shared/models/motions/workflow';
|
||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||
@ -343,6 +343,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
* @param promptService ensure safe deletion
|
||||
* @param pdfExport export the motion to pdf
|
||||
* @param personalNoteService: personal comments and favorite marker
|
||||
* @param linenumberingService The line numbering service
|
||||
* @param categoryRepo
|
||||
* @param userRepo
|
||||
*/
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { ModificationType } from 'app/core/ui-services/diff.service';
|
||||
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco';
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from './view-unified-change';
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change';
|
||||
|
||||
/**
|
||||
* Change recommendation class for the View
|
||||
*
|
||||
* Stores a motion including all (implicit) references
|
||||
* Provides "safe" access to variables and functions in {@link MotionChangeReco}
|
||||
* Provides "safe" access to variables and functions in {@link MotionChangeRecommendation}
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewMotionChangeRecommendation extends BaseViewModel implements ViewUnifiedChange {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from './view-unified-change';
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change';
|
||||
import { ViewMotion } from './view-motion';
|
||||
import { LineRange } from 'app/core/ui-services/diff.service';
|
||||
import { MergeAmendment } from 'app/shared/models/motions/workflow-state';
|
||||
@ -41,6 +41,8 @@ export class ViewMotionAmendedParagraph implements ViewUnifiedChange {
|
||||
* The state and recommendation of this amendment is considered.
|
||||
* The state takes precedence.
|
||||
*
|
||||
* HINT: This implementation should be consistent with get_amendment_merge_into_motion() in projector.py
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isAccepted(): boolean {
|
||||
|
@ -9,9 +9,9 @@ import { MotionPollService, CalculablePollKey } from './motion-poll.service';
|
||||
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
||||
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
|
||||
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
|
||||
import { ViewUnifiedChange } from '../models/view-unified-change';
|
||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
|
||||
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
|
||||
|
||||
/**
|
||||
* Type declaring which strings are valid options for metainfos to be exported into a pdf
|
||||
|
@ -1,6 +1,62 @@
|
||||
import { ChangeRecoMode, LineNumberingMode } from '../../../site/motions/models/view-motion';
|
||||
import { MergeAmendment } from '../../../shared/models/motions/workflow-state';
|
||||
|
||||
/**
|
||||
* This interface describes the data returned by the server about an amendment.
|
||||
* This object is used if actually the motion is shown and the amendment is shown in the context of the motion.
|
||||
*/
|
||||
export interface MotionsMotionSlideDataAmendment {
|
||||
id: number;
|
||||
title: string;
|
||||
amendment_paragraphs: string[];
|
||||
merge_amendment_into_final: MergeAmendment;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface describes the data returned by the server about a motion that is changed by an amendment.
|
||||
* It only contains the data necessary for rendering the amendment's diff.
|
||||
*/
|
||||
export interface MotionsMotionSlideDataBaseMotion {
|
||||
identifier: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface describes the data returned by the server about a statute paragraph that is changed by an amendment.
|
||||
* It only contains the data necessary for rendering the amendment's diff.
|
||||
*/
|
||||
export interface MotionsMotionSlideDataBaseStatute {
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface describes the data returned by the server about a change recommendation.
|
||||
*/
|
||||
export interface MotionsMotionSlideDataChangeReco {
|
||||
creation_time: string;
|
||||
id: number;
|
||||
internal: boolean;
|
||||
line_from: number;
|
||||
line_to: number;
|
||||
motion_id: number;
|
||||
other_description: string;
|
||||
rejected: false;
|
||||
text: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hint: defined on server-side in the file /openslides/motions/projector.py
|
||||
*
|
||||
* This interface describes either an motion (with all amendments and change recommendations enbedded)
|
||||
* or an amendment (with the bas motion embedded).
|
||||
*/
|
||||
export interface MotionsMotionSlideData {
|
||||
identifier: string;
|
||||
title: string;
|
||||
preamble: string;
|
||||
text: string;
|
||||
reason?: string;
|
||||
is_child: boolean;
|
||||
@ -9,7 +65,13 @@ export interface MotionsMotionSlideData {
|
||||
recommender?: string;
|
||||
recommendation?: string;
|
||||
recommendation_extension?: string;
|
||||
amendment_paragraphs: { paragraph: string }[];
|
||||
change_recommendations: object[];
|
||||
base_motion?: MotionsMotionSlideDataBaseMotion;
|
||||
base_statute?: MotionsMotionSlideDataBaseStatute;
|
||||
amendment_paragraphs: string[];
|
||||
change_recommendations: MotionsMotionSlideDataChangeReco[];
|
||||
amendments: MotionsMotionSlideDataAmendment[];
|
||||
modified_final_version?: string;
|
||||
line_length: number;
|
||||
line_numbering_mode: LineNumberingMode;
|
||||
change_recommendation_mode: ChangeRecoMode;
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change';
|
||||
import { MotionsMotionSlideDataAmendment } from './motions-motion-slide-data';
|
||||
import { MergeAmendment } from '../../../shared/models/motions/workflow-state';
|
||||
import { LineRange } from '../../../core/ui-services/diff.service';
|
||||
|
||||
/**
|
||||
* This class adds methods to the MotionsMotionSlideDataChangeReco data object
|
||||
* necessary for use it as a UnifiedChange in the Diff-Functions
|
||||
*/
|
||||
export class MotionsMotionSlideObjAmendmentParagraph implements ViewUnifiedChange {
|
||||
public id: number;
|
||||
public type: number;
|
||||
public merge_amendment_into_final: MergeAmendment;
|
||||
|
||||
public constructor(
|
||||
data: MotionsMotionSlideDataAmendment,
|
||||
private paragraphNo: number,
|
||||
private newText: string,
|
||||
private lineRange: LineRange
|
||||
) {
|
||||
this.id = data.id;
|
||||
this.merge_amendment_into_final = data.merge_amendment_into_final;
|
||||
}
|
||||
|
||||
public getChangeId(): string {
|
||||
return 'amendment-' + this.id.toString(10) + '-' + this.paragraphNo.toString(10);
|
||||
}
|
||||
|
||||
public getChangeType(): ViewUnifiedChangeType {
|
||||
return ViewUnifiedChangeType.TYPE_AMENDMENT;
|
||||
}
|
||||
|
||||
public getChangeNewText(): string {
|
||||
return this.newText;
|
||||
}
|
||||
|
||||
public getLineFrom(): number {
|
||||
return this.lineRange.from;
|
||||
}
|
||||
|
||||
public getLineTo(): number {
|
||||
return this.lineRange.to;
|
||||
}
|
||||
|
||||
public isAccepted(): boolean {
|
||||
return this.merge_amendment_into_final === MergeAmendment.YES;
|
||||
}
|
||||
|
||||
public isRejected(): boolean {
|
||||
return this.merge_amendment_into_final === MergeAmendment.NO;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change';
|
||||
import { MotionsMotionSlideDataChangeReco } from './motions-motion-slide-data';
|
||||
|
||||
/**
|
||||
* This class adds methods to the MotionsMotionSlideDataChangeReco data object
|
||||
* necessary for use it as a UnifiedChange in the Diff-Functions
|
||||
*/
|
||||
export class MotionsMotionSlideObjChangeReco implements MotionsMotionSlideDataChangeReco, ViewUnifiedChange {
|
||||
public creation_time: string;
|
||||
public id: number;
|
||||
public internal: boolean;
|
||||
public line_from: number;
|
||||
public line_to: number;
|
||||
public motion_id: number;
|
||||
public other_description: string;
|
||||
public rejected: false;
|
||||
public text: string;
|
||||
public type: number;
|
||||
|
||||
public constructor(data: MotionsMotionSlideDataChangeReco) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
public getChangeId(): string {
|
||||
return 'recommendation-' + this.id.toString(10);
|
||||
}
|
||||
|
||||
public getChangeNewText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
public getChangeType(): ViewUnifiedChangeType {
|
||||
return ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION;
|
||||
}
|
||||
|
||||
public getLineFrom(): number {
|
||||
return this.line_from;
|
||||
}
|
||||
|
||||
public getLineTo(): number {
|
||||
return this.line_to;
|
||||
}
|
||||
|
||||
public isAccepted(): boolean {
|
||||
return !this.rejected;
|
||||
}
|
||||
|
||||
public isRejected(): boolean {
|
||||
return this.rejected;
|
||||
}
|
||||
}
|
@ -20,12 +20,52 @@
|
||||
<h2><span translate>Motion</span> {{ data.data.identifier }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Text (original) -->
|
||||
<div *ngIf="!data.data.is_child" [innerHTML]="data.data.text"></div>
|
||||
<!-- Text -->
|
||||
<span class="text-prefix-label">{{ preamble | translate }}</span>
|
||||
|
||||
<!-- Regular motions or traditional amendments -->
|
||||
<ng-container *ngIf="!isStatuteAmendment() && !isParagraphBasedAmendment()">
|
||||
<div
|
||||
class="motion-text"
|
||||
[class.line-numbers-none]="isLineNumberingNone()"
|
||||
[class.line-numbers-inline]="isLineNumberingInline()"
|
||||
[class.line-numbers-outside]="isLineNumberingOutside()"
|
||||
>
|
||||
<div [innerHTML]="sanitizedText(getFormattedText())"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Statute amendments -->
|
||||
<div
|
||||
class="motion-text line-numbers-none"
|
||||
*ngIf="isStatuteAmendment()"
|
||||
[innerHTML]="getFormattedStatuteAmendment()"
|
||||
></div>
|
||||
|
||||
<!-- Amendment text -->
|
||||
<div *ngIf="data.data.is_child && data.data.amendment_paragraphs"
|
||||
[innerHTML]="data.data.amendment_paragraphs[0]"></div>
|
||||
<section class="text-holder" *ngIf="isParagraphBasedAmendment()">
|
||||
<div class="alert alert-info" *ngIf="getAmendedParagraphs().length === 0">
|
||||
<span translate>No changes at the text.</span>
|
||||
</div>
|
||||
<div
|
||||
*ngFor="let paragraph of getAmendedParagraphs()"
|
||||
class="motion-text motion-text-diff amendment-view"
|
||||
[class.line-numbers-none]="isLineNumberingNone()"
|
||||
[class.line-numbers-inline]="isLineNumberingInline()"
|
||||
[class.line-numbers-outside]="isLineNumberingOutside()"
|
||||
>
|
||||
<h3 *ngIf="paragraph.diffLineTo === paragraph.diffLineFrom + 1" class="amendment-line-header">
|
||||
<span translate>Line</span> {{ paragraph.diffLineFrom }}:
|
||||
</h3>
|
||||
<h3 *ngIf="paragraph.diffLineTo !== paragraph.diffLineFrom + 1" class="amendment-line-header">
|
||||
<span translate>Line</span> {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}:
|
||||
</h3>
|
||||
|
||||
<div class="paragraph-context" [innerHtml]="sanitizedText(paragraph.textPre)"></div>
|
||||
<div [innerHtml]="sanitizedText(paragraph.text)"></div>
|
||||
<div class="paragraph-context" [innerHtml]="sanitizedText(paragraph.textPost)"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reason -->
|
||||
<div *ngIf="data.data.reason">
|
||||
|
@ -1,3 +1,9 @@
|
||||
@import '../../../../assets/styles/motion-styles-common';
|
||||
|
||||
::ng-deep .paragraph-context {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#sidebox {
|
||||
width: 260px;
|
||||
right: 0;
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { MotionsMotionSlideData } from './motions-motion-slide-data';
|
||||
import { MotionsMotionSlideData, MotionsMotionSlideDataAmendment } from './motions-motion-slide-data';
|
||||
import { ChangeRecoMode, LineNumberingMode } from '../../../site/motions/models/view-motion';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { DiffLinesInParagraph, DiffService, LineRange } from '../../../core/ui-services/diff.service';
|
||||
import { LinenumberingService } from '../../../core/ui-services/linenumbering.service';
|
||||
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
|
||||
import { MotionsMotionSlideObjChangeReco } from './motions-motion-slide-obj-change-reco';
|
||||
import { SlideData } from '../../../site/projector/services/projector-data.service';
|
||||
import { MotionsMotionSlideObjAmendmentParagraph } from './motions-motion-slide-obj-amendment-paragraph';
|
||||
|
||||
@Component({
|
||||
selector: 'os-motions-motion-slide',
|
||||
@ -8,7 +16,336 @@ import { MotionsMotionSlideData } from './motions-motion-slide-data';
|
||||
styleUrls: ['./motions-motion-slide.component.scss']
|
||||
})
|
||||
export class MotionsMotionSlideComponent extends BaseSlideComponent<MotionsMotionSlideData> {
|
||||
public constructor() {
|
||||
/**
|
||||
* Indicates the LineNumberingMode Mode.
|
||||
*/
|
||||
public lnMode: LineNumberingMode;
|
||||
|
||||
/**
|
||||
* Indicates the Change reco Mode.
|
||||
*/
|
||||
public crMode: ChangeRecoMode;
|
||||
|
||||
/**
|
||||
* Indicates the maximum line length as defined in the configuration.
|
||||
*/
|
||||
public lineLength: number;
|
||||
|
||||
/**
|
||||
* Indicates the currently highlighted line, if any.
|
||||
* @TODO Read value from the backend
|
||||
*/
|
||||
public highlightedLine: number;
|
||||
|
||||
/**
|
||||
* Value of the config variable `motions_preamble`
|
||||
*/
|
||||
public preamble: string;
|
||||
|
||||
/**
|
||||
* All change recommendations AND amendments, sorted by line number.
|
||||
*/
|
||||
public allChangingObjects: ViewUnifiedChange[];
|
||||
|
||||
private _data: SlideData<MotionsMotionSlideData>;
|
||||
|
||||
@Input()
|
||||
public set data(value: SlideData<MotionsMotionSlideData>) {
|
||||
this._data = value;
|
||||
this.lnMode = value.data.line_numbering_mode;
|
||||
this.lineLength = value.data.line_length;
|
||||
this.preamble = value.data.preamble;
|
||||
this.crMode = value.element.mode || 'original';
|
||||
console.log(this.crMode);
|
||||
|
||||
this.recalcUnifiedChanges();
|
||||
}
|
||||
|
||||
public get data(): SlideData<MotionsMotionSlideData> {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private sanitizer: DomSanitizer,
|
||||
private lineNumbering: LinenumberingService,
|
||||
private diff: DiffService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all paragraphs that are affected by the given amendment as unified change objects.
|
||||
*
|
||||
* @param {MotionsMotionSlideDataAmendment} amendment
|
||||
* @returns {MotionsMotionSlideObjAmendmentParagraph[]}
|
||||
*/
|
||||
public getAmendmentAmendedParagraphs(
|
||||
amendment: MotionsMotionSlideDataAmendment
|
||||
): MotionsMotionSlideObjAmendmentParagraph[] {
|
||||
let baseHtml = this.data.data.text;
|
||||
baseHtml = this.lineNumbering.insertLineNumbers(baseHtml, this.lineLength);
|
||||
const baseParagraphs = this.lineNumbering.splitToParagraphs(baseHtml);
|
||||
|
||||
return amendment.amendment_paragraphs
|
||||
.map(
|
||||
(newText: string, paraNo: number): MotionsMotionSlideObjAmendmentParagraph => {
|
||||
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,
|
||||
this.lineLength,
|
||||
null,
|
||||
null,
|
||||
paragraphLines.from
|
||||
);
|
||||
newTextLines = this.diff.formatDiff(
|
||||
this.diff.extractRangeByLineNumbers(newTextLines, affectedLines.from, affectedLines.to)
|
||||
);
|
||||
|
||||
return new MotionsMotionSlideObjAmendmentParagraph(amendment, paraNo, newTextLines, affectedLines);
|
||||
}
|
||||
)
|
||||
.filter((para: MotionsMotionSlideObjAmendmentParagraph) => para !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges amendments and change recommendations and sorts them by the line numbers.
|
||||
* Called each time one of these arrays changes.
|
||||
*/
|
||||
private recalcUnifiedChanges(): void {
|
||||
this.allChangingObjects = [];
|
||||
|
||||
if (this.data.data.change_recommendations) {
|
||||
this.data.data.change_recommendations.forEach(change => {
|
||||
this.allChangingObjects.push(new MotionsMotionSlideObjChangeReco(change));
|
||||
});
|
||||
}
|
||||
if (this.data.data.amendments) {
|
||||
this.data.data.amendments.forEach(amendment => {
|
||||
const paras = this.getAmendmentAmendedParagraphs(amendment);
|
||||
paras.forEach(para => this.allChangingObjects.push(para));
|
||||
});
|
||||
}
|
||||
this.allChangingObjects.sort((a: ViewUnifiedChange, b: ViewUnifiedChange) => {
|
||||
if (a.getLineFrom() < b.getLineFrom()) {
|
||||
return -1;
|
||||
} else if (a.getLineFrom() > b.getLineFrom()) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if this is a statute amendment
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isStatuteAmendment(): boolean {
|
||||
return !!this.data.data.base_statute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if this is an paragraph-based amendment
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isParagraphBasedAmendment(): boolean {
|
||||
return (
|
||||
this.data.data.is_child &&
|
||||
this.data.data.amendment_paragraphs &&
|
||||
this.data.data.amendment_paragraphs.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if no line numbers are to be shown.
|
||||
*
|
||||
* @returns whether there are line numbers at all
|
||||
*/
|
||||
public isLineNumberingNone(): boolean {
|
||||
return this.lnMode === LineNumberingMode.None;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the line numbers are to be shown within the text with no line breaks.
|
||||
*
|
||||
* @returns whether the line numberings are inside
|
||||
*/
|
||||
public isLineNumberingInline(): boolean {
|
||||
return this.lnMode === LineNumberingMode.Inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the line numbers are to be shown to the left of the text.
|
||||
*
|
||||
* @returns whether the line numberings are outside
|
||||
*/
|
||||
public isLineNumberingOutside(): boolean {
|
||||
return this.lnMode === LineNumberingMode.Outside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the template to make a HTML string compatible with [innerHTML]
|
||||
* (otherwise line-number-data-attributes would be stripped out)
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {SafeHtml}
|
||||
*/
|
||||
public sanitizedText(text: string): SafeHtml {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a renderable HTML string representing the given line number range of this motion
|
||||
*
|
||||
* @param {string} motionHtml
|
||||
* @param {LineRange} lineRange
|
||||
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
|
||||
* @param {number} lineLength
|
||||
*/
|
||||
public extractMotionLineRange(
|
||||
motionHtml: string,
|
||||
lineRange: LineRange,
|
||||
lineNumbers: boolean,
|
||||
lineLength: number
|
||||
): string {
|
||||
const origHtml = this.lineNumbering.insertLineNumbers(motionHtml, this.lineLength, this.highlightedLine);
|
||||
const extracted = this.diff.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
|
||||
let html =
|
||||
extracted.outerContextStart +
|
||||
extracted.innerContextStart +
|
||||
extracted.html +
|
||||
extracted.innerContextEnd +
|
||||
extracted.outerContextEnd;
|
||||
if (lineNumbers) {
|
||||
html = this.lineNumbering.insertLineNumbers(html, lineLength, null, null, lineRange.from);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the formated motion text from the repository.
|
||||
*
|
||||
* @returns formated motion texts
|
||||
*/
|
||||
public getFormattedText(): string {
|
||||
// Prevent this.allChangingObjects to be reordered from within formatMotion
|
||||
// const changes: ViewUnifiedChange[] = Object.assign([], this.allChangingObjects);
|
||||
const motion = this.data.data;
|
||||
|
||||
switch (this.crMode) {
|
||||
case ChangeRecoMode.Original:
|
||||
return this.lineNumbering.insertLineNumbers(motion.text, this.lineLength, this.highlightedLine);
|
||||
case ChangeRecoMode.Changed:
|
||||
return this.diff.getTextWithChanges(
|
||||
motion.text,
|
||||
this.allChangingObjects,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
);
|
||||
case ChangeRecoMode.Diff:
|
||||
let text = '';
|
||||
this.allChangingObjects.forEach((change: ViewUnifiedChange, idx: number) => {
|
||||
if (idx === 0) {
|
||||
const lineRange = { from: 1, to: change.getLineFrom() };
|
||||
text += this.extractMotionLineRange(motion.text, lineRange, true, this.lineLength);
|
||||
} else if (this.allChangingObjects[idx - 1].getLineTo() < change.getLineFrom()) {
|
||||
const lineRange = {
|
||||
from: this.allChangingObjects[idx - 1].getLineTo(),
|
||||
to: change.getLineFrom()
|
||||
};
|
||||
text += this.extractMotionLineRange(motion.text, lineRange, true, this.lineLength);
|
||||
}
|
||||
text += this.diff.getChangeDiff(motion.text, change, this.lineLength, this.highlightedLine);
|
||||
});
|
||||
text += this.diff.getTextRemainderAfterLastChange(
|
||||
motion.text,
|
||||
this.allChangingObjects,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
);
|
||||
return text;
|
||||
case ChangeRecoMode.Final:
|
||||
const appliedChanges: ViewUnifiedChange[] = this.allChangingObjects.filter(change =>
|
||||
change.isAccepted()
|
||||
);
|
||||
return this.diff.getTextWithChanges(motion.text, appliedChanges, this.lineLength, this.highlightedLine);
|
||||
case ChangeRecoMode.ModifiedFinal:
|
||||
if (motion.modified_final_version) {
|
||||
return this.lineNumbering.insertLineNumbers(
|
||||
motion.modified_final_version,
|
||||
this.lineLength,
|
||||
this.highlightedLine,
|
||||
null,
|
||||
1
|
||||
);
|
||||
} else {
|
||||
// Use the final version as fallback, if the modified does not exist.
|
||||
const appliedChangeObjects: ViewUnifiedChange[] = this.allChangingObjects.filter(change =>
|
||||
change.isAccepted()
|
||||
);
|
||||
return this.diff.getTextWithChanges(
|
||||
motion.text,
|
||||
appliedChangeObjects,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
);
|
||||
}
|
||||
default:
|
||||
console.error('unrecognized ChangeRecoMode option (' + this.crMode + ')');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `this.data.data` is an amendment, this returns the list of all changed paragraphs.
|
||||
*
|
||||
* @returns {DiffLinesInParagraph[]}
|
||||
*/
|
||||
public getAmendedParagraphs(): DiffLinesInParagraph[] {
|
||||
let baseHtml = this.data.data.base_motion.text;
|
||||
baseHtml = this.lineNumbering.insertLineNumbers(baseHtml, this.lineLength);
|
||||
const baseParagraphs = this.lineNumbering.splitToParagraphs(baseHtml);
|
||||
|
||||
return this.data.data.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,
|
||||
this.lineLength
|
||||
);
|
||||
}
|
||||
)
|
||||
.filter((para: DiffLinesInParagraph) => para !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the diff html from the statute amendment, as SafeHTML for [innerHTML]
|
||||
*
|
||||
* @returns safe html strings
|
||||
*/
|
||||
public getFormattedStatuteAmendment(): SafeHtml {
|
||||
let diffHtml = this.diff.diff(this.data.data.base_statute.text, this.data.data.text);
|
||||
diffHtml = this.lineNumbering.insertLineBreaksWithoutNumbers(diffHtml, this.lineLength, true);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(diffHtml);
|
||||
}
|
||||
}
|
||||
|
131
client/src/assets/styles/motion-styles-common.scss
Normal file
131
client/src/assets/styles/motion-styles-common.scss
Normal file
@ -0,0 +1,131 @@
|
||||
/* Line numbers */
|
||||
// :host ::ng-deep is needed as this styling applies to the motion html that is injected using innerHTML,
|
||||
// which doesn't have the [ngcontent]-attributes necessary for regular styles.
|
||||
// An alternative approach (in case ::ng-deep gets removed) might be to change the view encapsulation.
|
||||
:host ::ng-deep .motion-text {
|
||||
ins,
|
||||
.insert {
|
||||
color: green;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
del,
|
||||
.delete {
|
||||
color: red;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: #ff0;
|
||||
}
|
||||
&.line-numbers-outside {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
.os-line-number {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
padding-right: 55px;
|
||||
|
||||
&:after {
|
||||
content: attr(data-line-number);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
vertical-align: top;
|
||||
color: gray;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&.selectable:hover:before,
|
||||
&.selected:before {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 20px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" fill="%23337ab7"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.line-numbers-inline {
|
||||
.os-line-break {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.os-line-number {
|
||||
display: inline-block;
|
||||
|
||||
&:after {
|
||||
display: inline-block;
|
||||
content: attr(data-line-number);
|
||||
vertical-align: top;
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
color: gray;
|
||||
margin-top: -3px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.line-numbers-none {
|
||||
.os-line-break {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.os-line-number {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.os-split-before {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.os-split-after {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
li.os-split-before {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .amendment-view {
|
||||
.os-split-after {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.os-split-before {
|
||||
margin-top: 0;
|
||||
}
|
||||
.paragraphcontext {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.amendment-context .paragraphcontext {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -32,6 +32,71 @@ def get_state(
|
||||
)
|
||||
|
||||
|
||||
def get_amendment_merge_into_motion(all_data, motion, amendment):
|
||||
"""
|
||||
HINT: This implementation should be consistent to isAccepted() in ViewMotionAmendedParagraph.ts
|
||||
"""
|
||||
if amendment["state_id"] is None:
|
||||
return 0
|
||||
|
||||
state = get_state(all_data, motion, amendment["state_id"])
|
||||
if (
|
||||
state["merge_amendment_into_final"] == -1
|
||||
or state["merge_amendment_into_final"] == 1
|
||||
):
|
||||
return state["merge_amendment_into_final"]
|
||||
|
||||
if amendment["recommendation_id"] is None:
|
||||
return 0
|
||||
recommendation = get_state(all_data, motion, amendment["recommendation_id"])
|
||||
return recommendation["merge_amendment_into_final"]
|
||||
|
||||
|
||||
def get_amendments_for_motion(motion, all_data):
|
||||
amendment_data = []
|
||||
for amendment_id, amendment in all_data["motions/motion"].items():
|
||||
if amendment["parent_id"] == motion["id"]:
|
||||
merge_amendment_into_final = get_amendment_merge_into_motion(
|
||||
all_data, motion, amendment
|
||||
)
|
||||
amendment_data.append(
|
||||
{
|
||||
"id": amendment["id"],
|
||||
"identifier": amendment["identifier"],
|
||||
"title": amendment["title"],
|
||||
"amendment_paragraphs": amendment["amendment_paragraphs"],
|
||||
"merge_amendment_into_final": merge_amendment_into_final,
|
||||
}
|
||||
)
|
||||
return amendment_data
|
||||
|
||||
|
||||
def get_amendment_base_motion(amendment, all_data):
|
||||
try:
|
||||
motion = all_data["motions/motion"][amendment["parent_id"]]
|
||||
except KeyError:
|
||||
motion_id = amendment["parent_id"]
|
||||
raise ProjectorElementException(f"motion with id {motion_id} does not exist")
|
||||
|
||||
return {
|
||||
"identifier": motion["identifier"],
|
||||
"title": motion["title"],
|
||||
"text": motion["text"],
|
||||
}
|
||||
|
||||
|
||||
def get_amendment_base_statute(amendment, all_data):
|
||||
try:
|
||||
statute = all_data["motions/statute-paragraph"][
|
||||
amendment["statute_paragraph_id"]
|
||||
]
|
||||
except KeyError:
|
||||
statute_id = amendment["statute_paragraph_id"]
|
||||
raise ProjectorElementException(f"statute with id {statute_id} does not exist")
|
||||
|
||||
return {"title": statute["title"], "text": statute["text"]}
|
||||
|
||||
|
||||
def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Motion slide.
|
||||
@ -51,7 +116,7 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
|
||||
* change_recommendations
|
||||
* submitter
|
||||
"""
|
||||
mode = element.get("mode")
|
||||
mode = element.get("mode", get_config(all_data, "motions_recommendation_text_mode"))
|
||||
motion_id = element.get("id")
|
||||
|
||||
if motion_id is None:
|
||||
@ -63,14 +128,44 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
|
||||
raise ProjectorElementException(f"motion with id {motion_id} does not exist")
|
||||
|
||||
show_meta_box = not get_config(all_data, "motions_disable_sidebox_on_projector")
|
||||
line_length = get_config(all_data, "motions_line_length")
|
||||
line_numbering_mode = get_config(all_data, "motions_default_line_numbering")
|
||||
motions_preamble = get_config(all_data, "motions_preamble")
|
||||
|
||||
if motion["statute_paragraph_id"]:
|
||||
change_recommendations = [] # type: ignore
|
||||
amendments = [] # type: ignore
|
||||
base_motion = None
|
||||
base_statute = get_amendment_base_statute(motion, all_data)
|
||||
elif bool(motion["parent_id"]) and motion["amendment_paragraphs"]:
|
||||
change_recommendations = []
|
||||
amendments = []
|
||||
base_motion = get_amendment_base_motion(motion, all_data)
|
||||
base_statute = None
|
||||
else:
|
||||
change_recommendations = list(
|
||||
filter(
|
||||
lambda reco: reco["internal"] is False, motion["change_recommendations"]
|
||||
)
|
||||
)
|
||||
amendments = get_amendments_for_motion(motion, all_data)
|
||||
base_motion = None
|
||||
base_statute = None
|
||||
|
||||
return_value = {
|
||||
"identifier": motion["identifier"],
|
||||
"title": motion["title"],
|
||||
"preamble": motions_preamble,
|
||||
"text": motion["text"],
|
||||
"amendment_paragraphs": motion["amendment_paragraphs"],
|
||||
"base_motion": base_motion,
|
||||
"base_statute": base_statute,
|
||||
"is_child": bool(motion["parent_id"]),
|
||||
"show_meta_box": show_meta_box,
|
||||
"change_recommendations": change_recommendations,
|
||||
"amendments": amendments,
|
||||
"line_length": line_length,
|
||||
"line_numbering_mode": line_numbering_mode,
|
||||
}
|
||||
|
||||
if not get_config(all_data, "motions_disable_reason_on_projector"):
|
||||
@ -98,7 +193,6 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return_value["recommender"] = get_config(
|
||||
all_data, "motions_recommendations_by"
|
||||
)
|
||||
return_value["change_recommendations"] = motion["change_recommendations"]
|
||||
|
||||
return_value["submitter"] = [
|
||||
get_user_name(all_data, submitter["user_id"])
|
||||
|
@ -74,7 +74,99 @@ def all_data():
|
||||
"weight": 10000,
|
||||
"created": "2019-01-19T18:37:34.741336+01:00",
|
||||
"last_modified": "2019-01-19T18:37:34.741368+01:00",
|
||||
}
|
||||
"change_recommendations": [
|
||||
{
|
||||
"id": 1,
|
||||
"motion_id": 1,
|
||||
"rejected": False,
|
||||
"internal": True,
|
||||
"type": 0,
|
||||
"other_description": "",
|
||||
"line_from": 1,
|
||||
"line_to": 2,
|
||||
"text": "internal new motion text",
|
||||
"creation_time": "2019-02-09T09:54:06.256378+01:00",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"motion_id": 1,
|
||||
"rejected": False,
|
||||
"internal": False,
|
||||
"type": 0,
|
||||
"other_description": "",
|
||||
"line_from": 1,
|
||||
"line_to": 2,
|
||||
"text": "public new motion text",
|
||||
"creation_time": "2019-02-09T09:54:06.256378+01:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
2: {
|
||||
"id": 2,
|
||||
"identifier": "Ä1",
|
||||
"title": "Amendment for 12345",
|
||||
"text": "",
|
||||
"amendment_paragraphs": ["New motion text"],
|
||||
"modified_final_version": "",
|
||||
"reason": "",
|
||||
"parent_id": 1,
|
||||
"category_id": None,
|
||||
"comments": [],
|
||||
"motion_block_id": None,
|
||||
"origin": "",
|
||||
"submitters": [{"id": 4, "user_id": 1, "motion_id": 1, "weight": 1}],
|
||||
"supporters_id": [],
|
||||
"state_id": 1,
|
||||
"state_extension": None,
|
||||
"state_access_level": 0,
|
||||
"statute_paragraph_id": None,
|
||||
"workflow_id": 1,
|
||||
"recommendation_id": None,
|
||||
"recommendation_extension": None,
|
||||
"tags_id": [],
|
||||
"attachments_id": [],
|
||||
"polls": [],
|
||||
"agenda_item_id": 4,
|
||||
"log_messages": [],
|
||||
"sort_parent_id": None,
|
||||
"weight": 10000,
|
||||
"created": "2019-01-19T18:37:34.741336+01:00",
|
||||
"last_modified": "2019-01-19T18:37:34.741368+01:00",
|
||||
"change_recommendations": [],
|
||||
},
|
||||
3: {
|
||||
"id": 3,
|
||||
"identifier": None,
|
||||
"title": "Statute amendment for §1 Preamble",
|
||||
"text": "<p>Some other preamble text</p>",
|
||||
"amendment_paragraphs": None,
|
||||
"modified_final_version": "",
|
||||
"reason": "",
|
||||
"parent_id": None,
|
||||
"category_id": None,
|
||||
"comments": [],
|
||||
"motion_block_id": None,
|
||||
"origin": "",
|
||||
"submitters": [{"id": 4, "user_id": 1, "motion_id": 1, "weight": 1}],
|
||||
"supporters_id": [],
|
||||
"state_id": 1,
|
||||
"state_extension": None,
|
||||
"state_access_level": 0,
|
||||
"statute_paragraph_id": 1,
|
||||
"workflow_id": 1,
|
||||
"recommendation_id": None,
|
||||
"recommendation_extension": None,
|
||||
"tags_id": [],
|
||||
"attachments_id": [],
|
||||
"polls": [],
|
||||
"agenda_item_id": 4,
|
||||
"log_messages": [],
|
||||
"sort_parent_id": None,
|
||||
"weight": 10000,
|
||||
"created": "2019-01-19T18:37:34.741336+01:00",
|
||||
"last_modified": "2019-01-19T18:37:34.741368+01:00",
|
||||
"change_recommendations": [],
|
||||
},
|
||||
}
|
||||
return_value["motions/workflow"] = {
|
||||
1: {
|
||||
@ -149,6 +241,14 @@ def all_data():
|
||||
"first_state_id": 1,
|
||||
}
|
||||
}
|
||||
return_value["motions/statute-paragraph"] = {
|
||||
1: {
|
||||
"id": 1,
|
||||
"title": "§1 Preamble",
|
||||
"text": "<p>Some preamble text</p>",
|
||||
"weight": 10000,
|
||||
}
|
||||
}
|
||||
return_value["motions/motion-change-recommendation"] = {}
|
||||
return return_value
|
||||
|
||||
@ -162,9 +262,85 @@ def test_motion_slide(all_data):
|
||||
"identifier": "4",
|
||||
"title": "12345",
|
||||
"text": "motion text",
|
||||
"amendments": [
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Amendment for 12345",
|
||||
"amendment_paragraphs": ["New motion text"],
|
||||
"identifier": "Ä1",
|
||||
"merge_amendment_into_final": 0,
|
||||
}
|
||||
],
|
||||
"amendment_paragraphs": None,
|
||||
"change_recommendations": [
|
||||
{
|
||||
"id": 2,
|
||||
"motion_id": 1,
|
||||
"rejected": False,
|
||||
"internal": False,
|
||||
"type": 0,
|
||||
"other_description": "",
|
||||
"line_from": 1,
|
||||
"line_to": 2,
|
||||
"text": "public new motion text",
|
||||
"creation_time": "2019-02-09T09:54:06.256378+01:00",
|
||||
}
|
||||
],
|
||||
"base_motion": None,
|
||||
"base_statute": None,
|
||||
"is_child": False,
|
||||
"show_meta_box": True,
|
||||
"reason": "",
|
||||
"submitter": ["Administrator"],
|
||||
"line_length": 90,
|
||||
"line_numbering_mode": "none",
|
||||
"preamble": "The assembly may decide:",
|
||||
}
|
||||
|
||||
|
||||
def test_amendment_slide(all_data):
|
||||
element: Dict[str, Any] = {"id": 2}
|
||||
|
||||
data = projector.motion_slide(all_data, element)
|
||||
|
||||
assert data == {
|
||||
"identifier": "Ä1",
|
||||
"title": "Amendment for 12345",
|
||||
"text": "",
|
||||
"amendments": [],
|
||||
"amendment_paragraphs": ["New motion text"],
|
||||
"change_recommendations": [],
|
||||
"base_motion": {"identifier": "4", "text": "motion text", "title": "12345"},
|
||||
"base_statute": None,
|
||||
"is_child": True,
|
||||
"show_meta_box": True,
|
||||
"reason": "",
|
||||
"submitter": ["Administrator"],
|
||||
"line_length": 90,
|
||||
"line_numbering_mode": "none",
|
||||
"preamble": "The assembly may decide:",
|
||||
}
|
||||
|
||||
|
||||
def test_statute_amendment_slide(all_data):
|
||||
element: Dict[str, Any] = {"id": 3}
|
||||
|
||||
data = projector.motion_slide(all_data, element)
|
||||
|
||||
assert data == {
|
||||
"identifier": None,
|
||||
"title": "Statute amendment for §1 Preamble",
|
||||
"text": "<p>Some other preamble text</p>",
|
||||
"amendments": [],
|
||||
"amendment_paragraphs": None,
|
||||
"change_recommendations": [],
|
||||
"base_motion": None,
|
||||
"base_statute": {"title": "§1 Preamble", "text": "<p>Some preamble text</p>"},
|
||||
"is_child": False,
|
||||
"show_meta_box": True,
|
||||
"reason": "",
|
||||
"submitter": ["Administrator"],
|
||||
"line_length": 90,
|
||||
"line_numbering_mode": "none",
|
||||
"preamble": "The assembly may decide:",
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user