Merge pull request #4246 from CatoTH/OS3-Slides-LineNumbering-Diff

Initial support for line numbering and CR in Projector
This commit is contained in:
Emanuel Schütze 2019-02-18 09:19:16 +01:00 committed by GitHub
commit 69539cacbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1110 additions and 283 deletions

View File

@ -27,7 +27,7 @@ import { TreeService } from 'app/core/ui-services/tree.service';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-change-recommendation'; import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-change-recommendation';
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph'; 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 { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { Workflow } from 'app/shared/models/motions/workflow'; import { Workflow } from 'app/shared/models/motions/workflow';
import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { WorkflowState } from 'app/shared/models/motions/workflow-state';
@ -397,7 +397,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
case ChangeRecoMode.Original: case ChangeRecoMode.Original:
return this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength, highlightLine); return this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength, highlightLine);
case ChangeRecoMode.Changed: case ChangeRecoMode.Changed:
return this.diff.getTextWithChanges(targetMotion, changes, lineLength, highlightLine); return this.diff.getTextWithChanges(targetMotion.text, changes, lineLength, highlightLine);
case ChangeRecoMode.Diff: case ChangeRecoMode.Diff:
let text = ''; let text = '';
changes.forEach((change: ViewUnifiedChange, idx: number) => { changes.forEach((change: ViewUnifiedChange, idx: number) => {
@ -424,13 +424,18 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
highlightLine 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; return text;
case ChangeRecoMode.Final: case ChangeRecoMode.Final:
const appliedChanges: ViewUnifiedChange[] = changes.filter(change => change.isAccepted()); 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: case ChangeRecoMode.ModifiedFinal:
if (targetMotion.modified_final_version) { if (targetMotion.modified_final_version) {
return this.lineNumbering.insertLineNumbers( return this.lineNumbering.insertLineNumbers(
@ -496,63 +501,6 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
return html; 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 * 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. * This object is not saved yet and does not yet have any changed HTML. It's meant to populate the UI form.
* *
* @param {number} motionId * @param {number} motionId
@ -590,66 +538,6 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
return new ViewMotionChangeRecommendation(changeReco); 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 * Given an amendment, this returns the motion affected by this amendments
* *

View File

@ -1,8 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { LinenumberingService } from './linenumbering.service'; import { LinenumberingService } from './linenumbering.service';
import { ViewMotion } from '../../site/motions/models/view-motion'; import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change';
import { ViewUnifiedChange } from '../../site/motions/models/view-unified-change';
const ELEMENT_NODE = 1; const ELEMENT_NODE = 1;
const TEXT_NODE = 3; const TEXT_NODE = 3;
@ -2036,18 +2035,18 @@ export class DiffService {
/** /**
* Applies all given changes to the motion and returns the (line-numbered) text * Applies all given changes to the motion and returns the (line-numbered) text
* *
* @param {ViewMotion} motion * @param {string} motionHtml
* @param {ViewUnifiedChange[]} changes * @param {ViewUnifiedChange[]} changes
* @param {number} lineLength * @param {number} lineLength
* @param {number} highlightLine * @param {number} highlightLine
*/ */
public getTextWithChanges( public getTextWithChanges(
motion: ViewMotion, motionHtml: string,
changes: ViewUnifiedChange[], changes: ViewUnifiedChange[],
lineLength: number, lineLength: number,
highlightLine: number highlightLine: number
): string { ): 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 need to be applied from the bottom up, to prevent conflicts with changing line numbers.
changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => { changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => {
@ -2127,4 +2126,120 @@ export class DiffService {
textPost: textPost textPost: textPost
} as DiffLinesInParagraph; } 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;
}
} }

View File

@ -5,7 +5,7 @@ import { Component } from '@angular/core';
import { LineNumberingMode, ViewMotion } from '../../models/view-motion'; import { LineNumberingMode, ViewMotion } from '../../models/view-motion';
import { MotionDetailDiffComponent } from './motion-detail-diff.component'; import { MotionDetailDiffComponent } from './motion-detail-diff.component';
import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.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 { Motion } from 'app/shared/models/motions/motion';
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation'; import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';

View File

@ -5,9 +5,9 @@ import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { LineNumberingMode, ViewMotion } from '../../models/view-motion'; 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 { 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 { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { import {
@ -69,6 +69,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param matSnackBar * @param matSnackBar
* @param sanitizer * @param sanitizer
* @param motionRepo * @param motionRepo
* @param diff
* @param recoRepo * @param recoRepo
* @param dialogService * @param dialogService
* @param configService * @param configService
@ -80,6 +81,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private motionRepo: MotionRepositoryService, private motionRepo: MotionRepositoryService,
private diff: DiffService,
private recoRepo: ChangeRecommendationRepositoryService, private recoRepo: ChangeRecommendationRepositoryService,
private dialogService: MatDialog, private dialogService: MatDialog,
private configService: ConfigService, private configService: ConfigService,
@ -142,7 +144,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param {ViewUnifiedChange} change * @param {ViewUnifiedChange} change
*/ */
public getDiff(change: ViewUnifiedChange): SafeHtml { 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); return this.sanitizer.bypassSecurityTrustHtml(html);
} }
@ -153,8 +155,8 @@ 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.motionRepo.getTextRemainderAfterLastChange( return this.diff.getTextRemainderAfterLastChange(
this.motion, this.motion.text,
this.changes, this.changes,
this.lineLength, this.lineLength,
this.highlightedLine this.highlightedLine

View File

@ -1,3 +1,5 @@
@import '../../../../../assets/styles/motion-styles-common';
span { span {
margin: 0; 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 { .main-nav-color {
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
} }

View File

@ -34,7 +34,7 @@ import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation'; import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
import { ViewCreateMotion } from '../../models/view-create-motion'; import { ViewCreateMotion } from '../../models/view-create-motion';
import { ViewportService } from 'app/core/ui-services/viewport.service'; 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 { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { Workflow } from 'app/shared/models/motions/workflow'; import { Workflow } from 'app/shared/models/motions/workflow';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; 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 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 categoryRepo * @param categoryRepo
* @param userRepo * @param userRepo
*/ */

View File

@ -1,13 +1,13 @@
import { BaseViewModel } from '../../base/base-view-model'; import { BaseViewModel } from '../../base/base-view-model';
import { ModificationType } from 'app/core/ui-services/diff.service'; import { ModificationType } from 'app/core/ui-services/diff.service';
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco'; 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 * Change recommendation class for the View
* *
* Stores a motion including all (implicit) references * 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 * @ignore
*/ */
export class ViewMotionChangeRecommendation extends BaseViewModel implements ViewUnifiedChange { export class ViewMotionChangeRecommendation extends BaseViewModel implements ViewUnifiedChange {

View File

@ -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 { ViewMotion } from './view-motion';
import { LineRange } from 'app/core/ui-services/diff.service'; import { LineRange } from 'app/core/ui-services/diff.service';
import { MergeAmendment } from 'app/shared/models/motions/workflow-state'; 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 and recommendation of this amendment is considered.
* The state takes precedence. * The state takes precedence.
* *
* HINT: This implementation should be consistent with get_amendment_merge_into_motion() in projector.py
*
* @returns {boolean} * @returns {boolean}
*/ */
public isAccepted(): boolean { public isAccepted(): boolean {

View File

@ -9,9 +9,9 @@ import { MotionPollService, CalculablePollKey } from './motion-poll.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service'; import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion'; 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 { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.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 * Type declaring which strings are valid options for metainfos to be exported into a pdf

View File

@ -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 { export interface MotionsMotionSlideData {
identifier: string; identifier: string;
title: string; title: string;
preamble: string;
text: string; text: string;
reason?: string; reason?: string;
is_child: boolean; is_child: boolean;
@ -9,7 +65,13 @@ export interface MotionsMotionSlideData {
recommender?: string; recommender?: string;
recommendation?: string; recommendation?: string;
recommendation_extension?: string; recommendation_extension?: string;
amendment_paragraphs: { paragraph: string }[]; base_motion?: MotionsMotionSlideDataBaseMotion;
change_recommendations: object[]; base_statute?: MotionsMotionSlideDataBaseStatute;
amendment_paragraphs: string[];
change_recommendations: MotionsMotionSlideDataChangeReco[];
amendments: MotionsMotionSlideDataAmendment[];
modified_final_version?: string; modified_final_version?: string;
line_length: number;
line_numbering_mode: LineNumberingMode;
change_recommendation_mode: ChangeRecoMode;
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -20,12 +20,52 @@
<h2><span translate>Motion</span> {{ data.data.identifier }}</h2> <h2><span translate>Motion</span> {{ data.data.identifier }}</h2>
</div> </div>
<!-- Text (original) --> <!-- Text -->
<div *ngIf="!data.data.is_child" [innerHTML]="data.data.text"></div> <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 --> <!-- Amendment text -->
<div *ngIf="data.data.is_child && data.data.amendment_paragraphs" <section class="text-holder" *ngIf="isParagraphBasedAmendment()">
[innerHTML]="data.data.amendment_paragraphs[0]"></div> <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 --> <!-- Reason -->
<div *ngIf="data.data.reason"> <div *ngIf="data.data.reason">

View File

@ -1,3 +1,9 @@
@import '../../../../assets/styles/motion-styles-common';
::ng-deep .paragraph-context {
opacity: 0.5;
}
#sidebox { #sidebox {
width: 260px; width: 260px;
right: 0; right: 0;

View File

@ -1,6 +1,14 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; 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({ @Component({
selector: 'os-motions-motion-slide', selector: 'os-motions-motion-slide',
@ -8,7 +16,336 @@ import { MotionsMotionSlideData } from './motions-motion-slide-data';
styleUrls: ['./motions-motion-slide.component.scss'] styleUrls: ['./motions-motion-slide.component.scss']
}) })
export class MotionsMotionSlideComponent extends BaseSlideComponent<MotionsMotionSlideData> { 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(); 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);
}
} }

View 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;
}
}

View File

@ -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]: def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
""" """
Motion slide. Motion slide.
@ -51,7 +116,7 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
* change_recommendations * change_recommendations
* submitter * submitter
""" """
mode = element.get("mode") mode = element.get("mode", get_config(all_data, "motions_recommendation_text_mode"))
motion_id = element.get("id") motion_id = element.get("id")
if motion_id is None: 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") raise ProjectorElementException(f"motion with id {motion_id} does not exist")
show_meta_box = not get_config(all_data, "motions_disable_sidebox_on_projector") 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 = { return_value = {
"identifier": motion["identifier"], "identifier": motion["identifier"],
"title": motion["title"], "title": motion["title"],
"preamble": motions_preamble,
"text": motion["text"], "text": motion["text"],
"amendment_paragraphs": motion["amendment_paragraphs"], "amendment_paragraphs": motion["amendment_paragraphs"],
"base_motion": base_motion,
"base_statute": base_statute,
"is_child": bool(motion["parent_id"]), "is_child": bool(motion["parent_id"]),
"show_meta_box": show_meta_box, "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"): 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( return_value["recommender"] = get_config(
all_data, "motions_recommendations_by" all_data, "motions_recommendations_by"
) )
return_value["change_recommendations"] = motion["change_recommendations"]
return_value["submitter"] = [ return_value["submitter"] = [
get_user_name(all_data, submitter["user_id"]) get_user_name(all_data, submitter["user_id"])

View File

@ -74,7 +74,99 @@ def all_data():
"weight": 10000, "weight": 10000,
"created": "2019-01-19T18:37:34.741336+01:00", "created": "2019-01-19T18:37:34.741336+01:00",
"last_modified": "2019-01-19T18:37:34.741368+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"] = { return_value["motions/workflow"] = {
1: { 1: {
@ -149,6 +241,14 @@ def all_data():
"first_state_id": 1, "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_value["motions/motion-change-recommendation"] = {}
return return_value return return_value
@ -162,9 +262,85 @@ def test_motion_slide(all_data):
"identifier": "4", "identifier": "4",
"title": "12345", "title": "12345",
"text": "motion text", "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, "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, "is_child": False,
"show_meta_box": True, "show_meta_box": True,
"reason": "", "reason": "",
"submitter": ["Administrator"], "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:",
} }