From 453fedbc3e0f96c6e85190034f03abe0bcdb0b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Ho=CC=88=C3=9Fl?= Date: Sun, 3 Feb 2019 19:30:07 +0100 Subject: [PATCH 1/3] Initial support for line numbering and change recommendations in Projector Read projector settings from the config Preamble, styling fixes Styling fixes Show amendments inside of the motion view Amendment view Projector and statute paragraphs Bugfix: Imports --- .../motions/motion-repository.service.ts | 134 +------ .../src/app/core/ui-services/diff.service.ts | 125 ++++++- .../models/motions}/view-unified-change.ts | 0 .../motion-detail-diff.component.spec.ts | 2 +- .../motion-detail-diff.component.ts | 12 +- .../motion-detail.component.scss | 134 +------ .../motion-detail/motion-detail.component.ts | 3 +- .../models/view-change-recommendation.ts | 4 +- .../models/view-motion-amended-paragraph.ts | 4 +- .../motions/services/motion-pdf.service.ts | 1 + .../motion/motions-motion-slide-data.ts | 66 +++- ...ns-motion-slide-obj-amendment-paragraph.ts | 52 +++ .../motions-motion-slide-obj-change-reco.ts | 51 +++ .../motions-motion-slide.component.html | 48 ++- .../motions-motion-slide.component.scss | 6 + .../motion/motions-motion-slide.component.ts | 342 +++++++++++++++++- .../assets/styles/motion-styles-common.scss | 131 +++++++ openslides/motions/projector.py | 88 ++++- 18 files changed, 923 insertions(+), 280 deletions(-) rename client/src/app/{site/motions/models => shared/models/motions}/view-unified-change.ts (100%) create mode 100644 client/src/app/slides/motions/motion/motions-motion-slide-obj-amendment-paragraph.ts create mode 100644 client/src/app/slides/motions/motion/motions-motion-slide-obj-change-reco.ts create mode 100644 client/src/assets/styles/motion-styles-common.scss diff --git a/client/src/app/core/repositories/motions/motion-repository.service.ts b/client/src/app/core/repositories/motions/motion-repository.service.ts index 200dd174c..04c6ec434 100644 --- a/client/src/app/core/repositories/motions/motion-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-repository.service.ts @@ -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 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 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 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 '' + msg + ''; - } - - 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 } /** - * 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 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 '' + msg + ''; - } - - 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 * diff --git a/client/src/app/core/ui-services/diff.service.ts b/client/src/app/core/ui-services/diff.service.ts index b42adeab3..c140a7cf3 100644 --- a/client/src/app/core/ui-services/diff.service.ts +++ b/client/src/app/core/ui-services/diff.service.ts @@ -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 '' + msg + ''; + } + + 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 '' + msg + ''; + } + + 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; + } } diff --git a/client/src/app/site/motions/models/view-unified-change.ts b/client/src/app/shared/models/motions/view-unified-change.ts similarity index 100% rename from client/src/app/site/motions/models/view-unified-change.ts rename to client/src/app/shared/models/motions/view-unified-change.ts diff --git a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.spec.ts b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.spec.ts index 0a5bc2eac..2c3a269ed 100644 --- a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.spec.ts +++ b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.spec.ts @@ -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'; diff --git a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts index 63a958cb2..3dc014880 100644 --- a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts +++ b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts @@ -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 diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss index a75067bcc..eb35c9829 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss @@ -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,'); - 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); } diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index c2b0527f7..82b34eaef 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -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 */ diff --git a/client/src/app/site/motions/models/view-change-recommendation.ts b/client/src/app/site/motions/models/view-change-recommendation.ts index 874881c58..fbb1a0c9a 100644 --- a/client/src/app/site/motions/models/view-change-recommendation.ts +++ b/client/src/app/site/motions/models/view-change-recommendation.ts @@ -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 { diff --git a/client/src/app/site/motions/models/view-motion-amended-paragraph.ts b/client/src/app/site/motions/models/view-motion-amended-paragraph.ts index 483787693..43199abac 100644 --- a/client/src/app/site/motions/models/view-motion-amended-paragraph.ts +++ b/client/src/app/site/motions/models/view-motion-amended-paragraph.ts @@ -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 { diff --git a/client/src/app/site/motions/services/motion-pdf.service.ts b/client/src/app/site/motions/services/motion-pdf.service.ts index 7b7a82795..b39d388a3 100644 --- a/client/src/app/site/motions/services/motion-pdf.service.ts +++ b/client/src/app/site/motions/services/motion-pdf.service.ts @@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ConfigService } from 'app/core/ui-services/config.service'; +import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change'; import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service'; import { MotionPollService, CalculablePollKey } from './motion-poll.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; diff --git a/client/src/app/slides/motions/motion/motions-motion-slide-data.ts b/client/src/app/slides/motions/motion/motions-motion-slide-data.ts index 75b2f650e..5838b0cad 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide-data.ts +++ b/client/src/app/slides/motions/motion/motions-motion-slide-data.ts @@ -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; } diff --git a/client/src/app/slides/motions/motion/motions-motion-slide-obj-amendment-paragraph.ts b/client/src/app/slides/motions/motion/motions-motion-slide-obj-amendment-paragraph.ts new file mode 100644 index 000000000..eda5acdd7 --- /dev/null +++ b/client/src/app/slides/motions/motion/motions-motion-slide-obj-amendment-paragraph.ts @@ -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; + } +} diff --git a/client/src/app/slides/motions/motion/motions-motion-slide-obj-change-reco.ts b/client/src/app/slides/motions/motion/motions-motion-slide-obj-change-reco.ts new file mode 100644 index 000000000..1247cd12f --- /dev/null +++ b/client/src/app/slides/motions/motion/motions-motion-slide-obj-change-reco.ts @@ -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; + } +} diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.html b/client/src/app/slides/motions/motion/motions-motion-slide.component.html index 1b6ed9317..ec8e6532d 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.html +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.html @@ -20,12 +20,52 @@

Motion {{ data.data.identifier }}

- -
+ + {{ preamble | translate }} + + + +
+
+
+
+ + +
-
+
+
+ No changes at the text. +
+
+

+ Line {{ paragraph.diffLineFrom }}: +

+

+ Line {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}: +

+ +
+
+
+
+
diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.scss b/client/src/app/slides/motions/motion/motions-motion-slide.component.scss index d7ca3f8c2..00e4d5d21 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.scss +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.scss @@ -1,3 +1,9 @@ +@import '../../../../assets/styles/motion-styles-common'; + +::ng-deep .paragraph-context { + opacity: 0.5; +} + #sidebox { width: 260px; right: 0; diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.ts b/client/src/app/slides/motions/motion/motions-motion-slide.component.ts index 080c4d6d6..d5e163b18 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.ts +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.ts @@ -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,335 @@ import { MotionsMotionSlideData } from './motions-motion-slide-data'; styleUrls: ['./motions-motion-slide.component.scss'] }) export class MotionsMotionSlideComponent extends BaseSlideComponent { - 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; + + @Input() + public set data(value: SlideData) { + this._data = value; + this.lnMode = value.data.line_numbering_mode; + this.lineLength = value.data.line_length; + this.crMode = value.data.change_recommendation_mode; + this.preamble = value.data.preamble; + + this.recalcUnifiedChanges(); + } + + public get data(): SlideData { + 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); + } } diff --git a/client/src/assets/styles/motion-styles-common.scss b/client/src/assets/styles/motion-styles-common.scss new file mode 100644 index 000000000..6b136ede3 --- /dev/null +++ b/client/src/assets/styles/motion-styles-common.scss @@ -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,'); + 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; + } +} diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index f33b607dc..8184fe2fe 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -31,6 +31,64 @@ def get_state( f"motion {motion['id']} can not be on the state with id {state_id}" ) +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]: """ @@ -63,14 +121,43 @@ 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") + change_recommendation_mode = get_config(all_data, "motions_recommendation_text_mode") + motions_preamble = get_config(all_data, "motions_preamble") + + if motion["statute_paragraph_id"]: + print("statute") + change_recommendations = [] + amendments = [] + 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"] == 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, + "change_recommendation_mode": change_recommendation_mode, + "line_length": line_length, + "line_numbering_mode": line_numbering_mode, } if not get_config(all_data, "motions_disable_reason_on_projector"): @@ -98,7 +185,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"]) From 35cd49e4fe73adfea3dc4163227e3a702b4d3140 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Thu, 14 Feb 2019 12:52:39 +0100 Subject: [PATCH 2/3] read crmode from the projector element --- client/src/app/site/motions/services/motion-pdf.service.ts | 3 +-- .../slides/motions/motion/motions-motion-slide.component.ts | 3 ++- openslides/motions/projector.py | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/src/app/site/motions/services/motion-pdf.service.ts b/client/src/app/site/motions/services/motion-pdf.service.ts index b39d388a3..5c19ed9a2 100644 --- a/client/src/app/site/motions/services/motion-pdf.service.ts +++ b/client/src/app/site/motions/services/motion-pdf.service.ts @@ -4,15 +4,14 @@ import { TranslateService } from '@ngx-translate/core'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ConfigService } from 'app/core/ui-services/config.service'; -import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change'; import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service'; 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 diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.ts b/client/src/app/slides/motions/motion/motions-motion-slide.component.ts index d5e163b18..6dbd5fc3f 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.ts +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.ts @@ -54,8 +54,9 @@ export class MotionsMotionSlideComponent extends BaseSlideComponent Dict[str, Any]: * change_recommendations * submitter """ - mode = element.get("mode") + mode = element.get("mode", "original") motion_id = element.get("id") if motion_id is None: @@ -123,7 +123,6 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: 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") - change_recommendation_mode = get_config(all_data, "motions_recommendation_text_mode") motions_preamble = get_config(all_data, "motions_preamble") if motion["statute_paragraph_id"]: @@ -155,7 +154,6 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: "show_meta_box": show_meta_box, "change_recommendations": change_recommendations, "amendments": amendments, - "change_recommendation_mode": change_recommendation_mode, "line_length": line_length, "line_numbering_mode": line_numbering_mode, } From 7b2e116f513d917ba4aa0e6c0c7d2279225253ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Ho=CC=88=C3=9Fl?= Date: Thu, 14 Feb 2019 16:35:25 +0100 Subject: [PATCH 3/3] Change reco default value, Slide test cases --- openslides/motions/projector.py | 60 +++++---- tests/unit/motions/test_projector.py | 178 ++++++++++++++++++++++++++- 2 files changed, 212 insertions(+), 26 deletions(-) diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index e67cb1923..9a02d1b21 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -31,6 +31,7 @@ def get_state( f"motion {motion['id']} can not be on the state with id {state_id}" ) + def get_amendment_merge_into_motion(all_data, motion, amendment): """ HINT: This implementation should be consistent to isAccepted() in ViewMotionAmendedParagraph.ts @@ -38,33 +39,38 @@ def get_amendment_merge_into_motion(all_data, motion, amendment): 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: + 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"] - ) + 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, - }) + 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"]] @@ -78,17 +84,18 @@ def get_amendment_base_motion(amendment, all_data): "text": motion["text"], } + def get_amendment_base_statute(amendment, all_data): try: - statute = all_data["motions/statute-paragraph"][amendment["statute_paragraph_id"]] + 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"], - } + return {"title": statute["title"], "text": statute["text"]} + def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: """ @@ -109,7 +116,7 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: * change_recommendations * submitter """ - mode = element.get("mode", "original") + mode = element.get("mode", get_config(all_data, "motions_recommendation_text_mode")) motion_id = element.get("id") if motion_id is None: @@ -126,9 +133,8 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: motions_preamble = get_config(all_data, "motions_preamble") if motion["statute_paragraph_id"]: - print("statute") - change_recommendations = [] - amendments = [] + 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"]: @@ -137,7 +143,11 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: base_motion = get_amendment_base_motion(motion, all_data) base_statute = None else: - change_recommendations = list(filter(lambda reco: reco["internal"] == False, motion["change_recommendations"])) + 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 diff --git a/tests/unit/motions/test_projector.py b/tests/unit/motions/test_projector.py index af1fcd1ce..c4311d031 100644 --- a/tests/unit/motions/test_projector.py +++ b/tests/unit/motions/test_projector.py @@ -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": "

Some other preamble text

", + "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": "

Some preamble text

", + "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": "

Some other preamble text

", + "amendments": [], + "amendment_paragraphs": None, + "change_recommendations": [], + "base_motion": None, + "base_statute": {"title": "§1 Preamble", "text": "

Some preamble text

"}, + "is_child": False, + "show_meta_box": True, + "reason": "", + "submitter": ["Administrator"], + "line_length": 90, + "line_numbering_mode": "none", + "preamble": "The assembly may decide:", }