Skip to content

Commit

Permalink
Merge pull request #7338 from donny-wong/v2.6.1
Browse files Browse the repository at this point in the history
V2.6.1
  • Loading branch information
donny-wong authored Dec 6, 2024
2 parents d37ab9f + 05daf49 commit bd4f447
Show file tree
Hide file tree
Showing 29 changed files with 534 additions and 73 deletions.
14 changes: 14 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [v2.6.1]

### ✨ New features and improvements

- Give instructors the ability to delete a TA from the Users Graders Tab (#7304)
- Added zoom and rotate functionality to PDF viewer (#7306)

### 🐛 Bug fixes

- Ensure we handle JSON parsing exceptions when converting Jupyter Notebooks (#7308)
- Fixed bug in grading context menu for editing/deleting annotations (#7314)
- Fixed bug in grading annotations table when deleting annotations (#7314)
- Ensure correct LTI version of lti_user_id is used on launch (#7335)

## [v2.6.0]

### ✨ New features and improvements
Expand Down
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ GEM
marcel (1.0.4)
matrix (0.4.2)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
mini_portile2 (2.8.8)
minitest (5.25.1)
mono_logger (1.1.2)
msgpack (1.7.2)
Expand All @@ -271,7 +271,7 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.16.7)
nokogiri (1.16.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
observer (0.1.2)
Expand Down Expand Up @@ -333,9 +333,9 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.1)
loofah (~> 2.21)
nokogiri (~> 1.14)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.3)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
Expand Down
2 changes: 1 addition & 1 deletion app/MARKUS_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION=v2.6.0,PATCH_LEVEL=DEV
VERSION=v2.6.1,PATCH_LEVEL=DEV
107 changes: 88 additions & 19 deletions app/assets/javascripts/Components/Result/pdf_viewer.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React from "react";
import {SingleSelectDropDown} from "../../DropDownMenu/SingleSelectDropDown";

export class PDFViewer extends React.PureComponent {
constructor(props) {
super(props);
this.pdfContainer = React.createRef();
this.state = {
zoom: "page-width",
rotation: 0, // NOTE: this is in degrees
};
}

componentDidMount() {
Expand All @@ -18,6 +23,8 @@ export class PDFViewer extends React.PureComponent {
if (this.props.resultView) {
this.eventBus.on("pagesinit", this.ready_annotations);
this.eventBus.on("pagesloaded", this.refresh_annotations);
} else {
this.eventBus.on("pagesloaded", this.update_pdf_view);
}

if (this.props.url) {
Expand All @@ -31,6 +38,8 @@ export class PDFViewer extends React.PureComponent {
} else {
if (this.props.resultView) {
this.refresh_annotations();
} else {
this.update_pdf_view();
}
}
}
Expand All @@ -44,7 +53,6 @@ export class PDFViewer extends React.PureComponent {
ready_annotations = () => {
annotation_type = ANNOTATION_TYPES.PDF;

this.pdfViewer.currentScaleValue = "page-width";
window.annotation_manager = new PdfAnnotationManager(!this.props.released_to_students);
window.annotation_manager.resetAngle();
this.annotation_manager = window.annotation_manager;
Expand All @@ -61,15 +69,35 @@ export class PDFViewer extends React.PureComponent {
window.pdfViewer = undefined;
}

update_pdf_view = () => {
if (
!!document.getElementById("pdfContainer") &&
!!document.getElementById("pdfContainer").offsetParent
) {
this.pdfViewer.currentScaleValue = this.state.zoom;
this.pdfViewer.pagesRotation = this.state.rotation;
}
};

refresh_annotations = () => {
$(".annotation_holder").remove();
this.pdfViewer.currentScaleValue = "page-width";
this.update_pdf_view();
this.props.annotations.forEach(this.display_annotation);
if (!!this.props.annotationFocus) {
document.getElementById("annotation_holder_" + this.props.annotationFocus).scrollIntoView();
}
};

rotate = () => {
if (this.props.resultView) {
annotation_manager.rotateClockwise90();
}

this.setState(({rotation}) => ({
rotation: (rotation + 90) % 360,
}));
};

display_annotation = annotation => {
if (annotation.x_range === undefined || annotation.y_range === undefined) {
return;
Expand Down Expand Up @@ -101,31 +129,72 @@ export class PDFViewer extends React.PureComponent {
);
};

rotate = () => {
annotation_manager.rotateClockwise90();
this.pdfViewer.rotatePages(90);
getZoomValuesToDisplayName = () => {
// 25-200 in increments of 25
const zoomLevels = Array.from({length: (200 - 25) / 25 + 1}, (_, i) =>
((i * 25 + 25) / 100).toFixed(2)
);

const valueToDisplayName = zoomLevels.reduce(
(acc, value) => {
acc[value] = `${(value * 100).toFixed(0)} %`;
return acc;
},
{"page-width": I18n.t("results.fit_to_page_width")}
);

return valueToDisplayName;
};

render() {
const cursor = this.props.released_to_students ? "default" : "crosshair";
const userSelect = this.props.released_to_students ? "default" : "none";
const zoomValuesToDisplayName = this.getZoomValuesToDisplayName();

return (
<div className="pdfContainerParent">
<div
id="pdfContainer"
className="pdfContainer"
style={{cursor, userSelect}}
ref={this.pdfContainer}
>
<div id="viewer" className="pdfViewer" />
<React.Fragment>
<div className="toolbar">
<div className="toolbar-actions">
{I18n.t("results.current_rotation", {rotation: this.state.rotation})}
<button onClick={this.rotate} className={"inline-button"}>
{I18n.t("results.rotate_image")}
</button>
<span style={{marginLeft: "7px"}}>{I18n.t("results.zoom")}</span>
<SingleSelectDropDown
valueToDisplayName={zoomValuesToDisplayName}
options={Object.keys(zoomValuesToDisplayName)}
selected={this.state.zoom}
dropdownStyle={{
minWidth: "auto",
width: "fit-content",
marginLeft: "5px",
verticalAlign: "middle",
}}
selectionStyle={{width: "90px", marginRight: "0px"}}
hideXMark={true}
onSelect={selection => {
this.setState({zoom: selection});
}}
/>
</div>
</div>
<div className="pdfContainerParent">
<div
key="sel_box"
id="sel_box"
className="annotation-holder-active"
style={{display: "none"}}
/>
id="pdfContainer"
className="pdfContainer"
style={{cursor, userSelect}}
ref={this.pdfContainer}
>
<div id="viewer" className="pdfViewer" />
<div
key="sel_box"
id="sel_box"
className="annotation-holder-active"
style={{display: "none"}}
/>
</div>
</div>
</div>
</React.Fragment>
);
}
}
116 changes: 116 additions & 0 deletions app/assets/javascripts/Components/__tests__/pdf_viewer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from "react";
import {render, screen, fireEvent} from "@testing-library/react";
import {PDFViewer} from "../Result/pdf_viewer";

describe("PDFViewer", () => {
let mockPdfViewer;
let mockAnnotationManager;

beforeEach(() => {
mockPdfViewer = {
setDocument: jest.fn(),
pagesRotation: 0,
currentScaleValue: "page-width",
};

mockAnnotationManager = {
rotateClockwise90: jest.fn(),
};

global.pdfjsViewer = {
EventBus: class {
on = jest.fn();
},
PDFViewer: jest.fn(() => mockPdfViewer),
};

global.annotation_manager = mockAnnotationManager;

render(<PDFViewer resultView={true} annotations={[]} />);
});

afterEach(() => {
jest.restoreAllMocks();
delete global.pdfjsViewer;
delete global.annotation_manager;
});

describe("rotation", () => {
let rotateButton;

beforeEach(() => {
rotateButton = screen.getByText(I18n.t("results.rotate_image"));
});

it("initially has a rotation of 0", async () => {
expect(mockPdfViewer.pagesRotation).toBe(0);
});

it("rotates to 90 degrees when rotate button is clicked once", () => {
fireEvent.click(rotateButton);

expect(mockAnnotationManager.rotateClockwise90).toHaveBeenCalledTimes(1);
expect(mockPdfViewer.pagesRotation).toBe(90);
});

it("rotates back to 0 degrees when rotate button is clicked four times", () => {
for (let i = 0; i < 4; i++) {
fireEvent.click(rotateButton);
}

expect(mockAnnotationManager.rotateClockwise90).toHaveBeenCalledTimes(4);
expect(mockPdfViewer.pagesRotation).toBe(0);
});
});

describe("zoom", () => {
it("has default zoom 'page-width' on initial render", () => {
expect(mockPdfViewer.currentScaleValue).toBe("page-width");
});

it("updates zoom to 100% (1.0) when the option is selected from dropdown", () => {
const dropdown = screen.getByTestId("dropdown");
fireEvent.click(dropdown);

const option100 = screen.getByText("100 %");
fireEvent.click(option100);

expect(mockPdfViewer.currentScaleValue).toBe("1.00");
});

it("updates zoom to 75% (0.75) when the option is selected from dropdown", () => {
const dropdown = screen.getByTestId("dropdown");
fireEvent.click(dropdown);

const option110 = screen.getByText("75 %");
fireEvent.click(option110);

expect(mockPdfViewer.currentScaleValue).toBe("0.75");
});

it("updates zoom to 125% (1.25) when the option is selected from dropdown", () => {
const dropdown = screen.getByTestId("dropdown");
fireEvent.click(dropdown);

const option120 = screen.getByText("125 %");
fireEvent.click(option120);

expect(mockPdfViewer.currentScaleValue).toBe("1.25");
});

it("resets zoom to 'page-width' when the option is selected after selecting another zoom", () => {
// set some arbitrary zoom first
const dropdown = screen.getByTestId("dropdown");
fireEvent.click(dropdown);
const option120 = screen.getByText("125 %");
fireEvent.click(option120);

// now put it back to page width
fireEvent.click(dropdown);
const fitToPageWidthOption = screen.getByText(I18n.t("results.fit_to_page_width"));
fireEvent.click(fitToPageWidthOption);

expect(mockPdfViewer.currentScaleValue).toBe("page-width");
});
});
});
Loading

0 comments on commit bd4f447

Please sign in to comment.