Skip to content
Snippets Groups Projects
Commit 46d22101 authored by RvdK's avatar RvdK
Browse files

Merge branch 'combinatory-fixes' into 'develop'

Refactorings and compatibility fix

See merge request !15
parents e1390b08 64ed5c0e
No related branches found
No related tags found
1 merge request!15Refactorings and compatibility fix
Pipeline #18063 passed
...@@ -30,7 +30,8 @@ div.mcq-option img.mcq-box { ...@@ -30,7 +30,8 @@ div.mcq-option img.mcq-box {
.editor-content { .editor-content {
background-color: #ddd; background-color: #ddd;
border-radius: 10px border-radius: 10px;
height: 100%;
} }
.selection-area { .selection-area {
......
...@@ -42,7 +42,15 @@ class Exams extends React.Component { ...@@ -42,7 +42,15 @@ class Exams extends React.Component {
page: problem.page, page: problem.page,
name: problem.name, name: problem.name,
graded: problem.graded, graded: problem.graded,
mc_options: problem.mc_options, mc_options: problem.mc_options.map((option) => {
option.cbOffsetX = 7 // checkbox offset relative to option position on x axis
option.cbOffsetY = 21 // checkbox offset relative to option position on y axis
option.widget.x -= option.cbOffsetX
option.widget.y -= option.cbOffsetY
return option
}),
widthMCO: 24,
heightMCO: 38,
isMCQ: problem.mc_options && problem.mc_options.length !== 0 // is the problem a mc question - used to display PanelMCQ isMCQ: problem.mc_options && problem.mc_options.length !== 0 // is the problem a mc question - used to display PanelMCQ
} }
} }
...@@ -178,6 +186,7 @@ class Exams extends React.Component { ...@@ -178,6 +186,7 @@ class Exams extends React.Component {
numPages={this.state.numPages} numPages={this.state.numPages}
onPDFLoad={this.onPDFLoad} onPDFLoad={this.onPDFLoad}
updateWidget={this.updateWidget} updateWidget={this.updateWidget}
updateMCWidget={this.updateMCWidget}
selectedWidgetId={this.state.selectedWidgetId} selectedWidgetId={this.state.selectedWidgetId}
selectWidget={(widgetId) => { selectWidget={(widgetId) => {
this.setState({ this.setState({
...@@ -185,7 +194,6 @@ class Exams extends React.Component { ...@@ -185,7 +194,6 @@ class Exams extends React.Component {
}) })
}} }}
createNewWidget={this.createNewWidget} createNewWidget={this.createNewWidget}
updateMCWidgetPosition={this.updateMCWidgetPosition}
updateExam={() => { updateExam={() => {
this.props.updateExam(this.props.examID) this.props.updateExam(this.props.examID)
}} }}
...@@ -290,11 +298,11 @@ class Exams extends React.Component { ...@@ -290,11 +298,11 @@ class Exams extends React.Component {
} }
/** /**
* This method creates a widget object and adds it to the corresponding problem * This method creates a mc option widget object and adds it to the corresponding problem
* @param problemWidget The widget the mc option belongs to * @param problemWidget The widget the mc option belongs to
* @param data the mc option * @param data the mc option
*/ */
createNewMCOWidget = (problemWidget, data) => { createNewMCWidget = (problemWidget, data) => {
this.setState((prevState) => { this.setState((prevState) => {
return { return {
widgets: update(prevState.widgets, { widgets: update(prevState.widgets, {
...@@ -316,12 +324,12 @@ class Exams extends React.Component { ...@@ -316,12 +324,12 @@ class Exams extends React.Component {
* @param widget the problem widget that includes the mcq widget * @param widget the problem widget that includes the mcq widget
* @param data the new location of the mcq widget (the location of the top-left corner) * @param data the new location of the mcq widget (the location of the top-left corner)
*/ */
updateMCWidgetPosition = (widget, data) => { updateMCWidget = (widget, data) => {
let newMCO = widget.problem.mc_options.map((option, i) => { let newMCO = widget.problem.mc_options.map((option, i) => {
return { return {
'widget': { 'widget': {
'x': { 'x': {
$set: data.x + i * 24 $set: data.x + i * widget.problem.widthMCO
}, },
'y': { 'y': {
// each mc option needs to be positioned next to the previous option and should not overlap it // each mc option needs to be positioned next to the previous option and should not overlap it
...@@ -359,6 +367,8 @@ class Exams extends React.Component { ...@@ -359,6 +367,8 @@ class Exams extends React.Component {
'label': labels[index], 'label': labels[index],
'problem_id': problemWidget.problem.id, 'problem_id': problemWidget.problem.id,
'feedback_id': null, 'feedback_id': null,
'cbOffsetX': 7, // checkbox offset relative to option position on x axis
'cbOffsetY': 21, // checkbox offset relative to option position on y axis
'widget': { 'widget': {
'name': 'mc_option_' + labels[index], 'name': 'mc_option_' + labels[index],
'x': xPos, 'x': xPos,
...@@ -369,14 +379,14 @@ class Exams extends React.Component { ...@@ -369,14 +379,14 @@ class Exams extends React.Component {
const formData = new window.FormData() const formData = new window.FormData()
formData.append('name', data.widget.name) formData.append('name', data.widget.name)
formData.append('x', data.widget.x) formData.append('x', data.widget.x + data.cbOffsetX)
formData.append('y', data.widget.y) formData.append('y', data.widget.y + data.cbOffsetY)
formData.append('problem_id', data.problem_id) formData.append('problem_id', data.problem_id)
formData.append('label', data.label) formData.append('label', data.label)
api.put('mult-choice/', formData).then(result => { api.put('mult-choice/', formData).then(result => {
data.id = result.mult_choice_id data.id = result.mult_choice_id
this.createNewMCOWidget(problemWidget, data) this.createNewMCWidget(problemWidget, data)
this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + 24, yPos) this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + problemWidget.problem.widthMCO, yPos)
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
}) })
...@@ -386,13 +396,14 @@ class Exams extends React.Component { ...@@ -386,13 +396,14 @@ class Exams extends React.Component {
const selectedWidgetId = this.state.selectedWidgetId const selectedWidgetId = this.state.selectedWidgetId
let selectedWidget = selectedWidgetId && this.state.widgets[selectedWidgetId] let selectedWidget = selectedWidgetId && this.state.widgets[selectedWidgetId]
let problem = selectedWidget && selectedWidget.problem let problem = selectedWidget && selectedWidget.problem
let widgetEditDisabled = this.state.previewing || !problem let containsMCOptions = (problem && problem.mc_options.length > 0) || false
let widgetEditDisabled = (this.state.previewing || !problem) || (this.props.exam.finalized && containsMCOptions)
let isGraded = problem && problem.graded let isGraded = problem && problem.graded
let widgetDeleteDisabled = widgetEditDisabled || isGraded let widgetDeleteDisabled = widgetEditDisabled || isGraded
let totalNrAnswers = 12 // the upper limit for the nr of possible answer boxes let totalNrAnswers = 12 // the upper limit for the nr of possible answer boxes
let containsMCOptions = (problem && problem.mc_options.length > 0) || false
let disabledDeleteBoxes = !containsMCOptions let disabledDeleteBoxes = !containsMCOptions
let isMCQ = (problem && problem.isMCQ) || false let isMCQ = (problem && problem.isMCQ) || false
let showPanelMCQ = isMCQ && !this.state.previewing && !this.props.exam.finalized
return ( return (
<React.Fragment> <React.Fragment>
...@@ -437,7 +448,7 @@ class Exams extends React.Component { ...@@ -437,7 +448,7 @@ class Exams extends React.Component {
} }
} }
/> />
{ isMCQ ? ( { showPanelMCQ ? (
<PanelMCQ <PanelMCQ
totalNrAnswers={totalNrAnswers} totalNrAnswers={totalNrAnswers}
disabledGenerateBoxes={containsMCOptions} disabledGenerateBoxes={containsMCOptions}
......
...@@ -89,6 +89,8 @@ class ExamEditor extends React.Component { ...@@ -89,6 +89,8 @@ class ExamEditor extends React.Component {
name: 'New problem', // TODO: Name name: 'New problem', // TODO: Name
page: this.props.page, page: this.props.page,
mc_options: [], mc_options: [],
widthMCO: 24,
heightMCO: 38,
isMCQ: false isMCQ: false
} }
const widgetData = { const widgetData = {
...@@ -178,8 +180,8 @@ class ExamEditor extends React.Component { ...@@ -178,8 +180,8 @@ class ExamEditor extends React.Component {
* @param widget the widget that was relocated * @param widget the widget that was relocated
* @param data the new location * @param data the new location
*/ */
updateWidgetPositionDB = (widget, data) => { updateWidgetDB = (widget, data) => {
api.patch('widgets/' + widget.id, data).then(() => { return api.patch('widgets/' + widget.id, data).then(() => {
// ok // ok
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
...@@ -187,15 +189,83 @@ class ExamEditor extends React.Component { ...@@ -187,15 +189,83 @@ class ExamEditor extends React.Component {
this.props.updateExam() this.props.updateExam()
}) })
} }
/** /**
* This function renders a group of options into one draggable widget * This function updates the state and the Database with the positions of the mc options.
* @returns {*} * @param widget the problem widget the mc options belong to
* @param data the new position of the mc widget
*/
updateMCO = (widget, data) => {
// update state
this.props.updateMCWidget(widget, {
x: Math.round(data.x),
y: Math.round(data.y)
})
// update DB
widget.problem.mc_options.forEach(
(option, i) => {
let newData = {
x: Math.round(data.x) + i * widget.problem.widthMCO + option.cbOffsetX,
y: Math.round(data.y) + option.cbOffsetY
}
this.updateWidgetDB(option, newData)
})
}
/**
* This function updates the position of the mc options inside when the corresponding problem widget changes in
* size or position. Note that the positions in the database are not updated. These should be updated once when the
* action (resizing/dragging/other) is finalized.
* @param widget the problem widget containing mc options
* @param data the new data about the new size/position of the problem widget
*/
repositionMC = (widget, data) => {
if (widget.problem.mc_options.length > 0) {
let oldX = widget.problem.mc_options[0].widget.x
let oldY = widget.problem.mc_options[0].widget.y
let newX = oldX
let newY = oldY
let widthOption = widget.problem.widthMCO * widget.problem.mc_options.length
let heightOption = widget.problem.heightMCO
let widthProblem = data.width ? data.width : widget.width
let heightProblem = data.height ? data.height : widget.height
if (newX < data.x) {
newX = data.x
} else if (newX + widthOption > data.x + widthProblem) {
newX = data.x + widget.width - widthOption
}
if (newY < data.y) {
newY = data.y
} else if (newY + heightOption > data.y + heightProblem) {
newY = data.y + widget.height - heightOption
}
let changed = (oldX !== newX) || (oldY !== newY) // update the state only if the mc options were moved
if (changed) {
this.props.updateMCWidget(widget, {
x: Math.round(newX),
y: Math.round(newY)
})
}
}
}
/**
* This function renders a group of options into one draggable widget.
* @param widget the problem widget that contains a mc options
* @return a react component representing the multiple choice widget
*/ */
renderMCWidget = (widget) => { renderMCWidget = (widget) => {
let width = 24 * widget.problem.mc_options.length let width = widget.problem.widthMCO * widget.problem.mc_options.length
let height = 38 let height = widget.problem.heightMCO
let enableResizing = false let enableResizing = false
const isSelected = widget.id === this.props.selectedWidgetId const isSelected = widget.id === this.props.selectedWidgetId
let xPos = widget.problem.mc_options[0].widget.x
let yPos = widget.problem.mc_options[0].widget.y
return ( return (
<ResizeAndDrag <ResizeAndDrag
key={'widget_mc_' + widget.id} key={'widget_mc_' + widget.id}
...@@ -213,8 +283,8 @@ class ExamEditor extends React.Component { ...@@ -213,8 +283,8 @@ class ExamEditor extends React.Component {
topRight: enableResizing topRight: enableResizing
}} }}
position={{ position={{
x: widget.problem.mc_options[0].widget.x, x: xPos,
y: widget.problem.mc_options[0].widget.y y: yPos
}} }}
size={{ size={{
width: width, width: width,
...@@ -224,19 +294,7 @@ class ExamEditor extends React.Component { ...@@ -224,19 +294,7 @@ class ExamEditor extends React.Component {
this.props.selectWidget(widget.id) this.props.selectWidget(widget.id)
}} }}
onDragStop={(e, data) => { onDragStop={(e, data) => {
this.props.updateMCWidgetPosition(widget, { this.updateMCO(widget, data)
x: Math.round(data.x),
y: Math.round(data.y)
})
widget.problem.mc_options.forEach(
(option, i) => {
let newData = {
x: Math.round(data.x),
y: Math.round(data.y) + i * 24
}
this.updateWidgetPositionDB(option, newData)
})
}} }}
> >
<div className={isSelected ? 'mcq-widget widget selected' : 'mcq-widget widget '}> <div className={isSelected ? 'mcq-widget widget selected' : 'mcq-widget widget '}>
...@@ -256,9 +314,9 @@ class ExamEditor extends React.Component { ...@@ -256,9 +314,9 @@ class ExamEditor extends React.Component {
} }
/** /**
* Render problem widget and the mc options that correspond to the problem * Render problem widget and the mc options that correspond to the problem.
* @param widget the corresponding widget object from the db * @param widget the corresponding widget object from the db
* @returns {Array} * @returns {Array} an array of react components to be displayed
*/ */
renderProblemWidget = (widget) => { renderProblemWidget = (widget) => {
// Only render when numPage is set // Only render when numPage is set
...@@ -300,38 +358,49 @@ class ExamEditor extends React.Component { ...@@ -300,38 +358,49 @@ class ExamEditor extends React.Component {
x: { $set: Math.round(position.x) }, x: { $set: Math.round(position.x) },
y: { $set: Math.round(position.y) } y: { $set: Math.round(position.y) }
}) })
this.repositionMC(widget, {
width: ref.offsetWidth,
height: ref.offsetHeight,
x: Math.round(position.x),
y: Math.round(position.y)
})
}} }}
onResizeStop={(e, direction, ref, delta, position) => { onResizeStop={(e, direction, ref, delta, position) => {
api.patch('widgets/' + widget.id, { this.updateWidgetDB(widget, {
x: Math.round(position.x), x: Math.round(position.x),
y: Math.round(position.y), y: Math.round(position.y),
width: ref.offsetWidth, width: ref.offsetWidth,
height: ref.offsetHeight height: ref.offsetHeight
}).then(() => { }).then(() => {
// ok if (widget.problem.mc_options.length > 0) {
}).catch(err => { this.updateMCO(widget, {
console.log(err) x: widget.problem.mc_options[0].widget.x, // these are guaranteed to be up to date
// update to try and get a consistent state y: widget.problem.mc_options[0].widget.y
this.props.updateExam() })
}
}) })
}} }}
onDragStart={() => { onDragStart={() => {
this.props.selectWidget(widget.id) this.props.selectWidget(widget.id)
}} }}
onDrag={(e, data) => this.repositionMC(widget, data)}
onDragStop={(e, data) => { onDragStop={(e, data) => {
this.props.updateWidget(widget.id, { this.props.updateWidget(widget.id, {
x: { $set: Math.round(data.x) }, x: { $set: Math.round(data.x) },
y: { $set: Math.round(data.y) } y: { $set: Math.round(data.y) }
}) })
api.patch('widgets/' + widget.id, { this.updateWidgetDB(widget, {
x: Math.round(data.x), x: Math.round(data.x),
y: Math.round(data.y) y: Math.round(data.y)
}).then(() => { }).then(() => {
// ok if (widget.problem.mc_options.length > 0) {
}).catch(err => { this.updateMCO(widget, {
console.log(err) // react offers the guarantee that setState calls are processed before handling next event
// update to try and get a consistent state // therefore the data in the state is up to date
this.props.updateExam() x: widget.problem.mc_options[0].widget.x,
y: widget.problem.mc_options[0].widget.y
})
}
}) })
}} }}
> >
...@@ -352,7 +421,7 @@ class ExamEditor extends React.Component { ...@@ -352,7 +421,7 @@ class ExamEditor extends React.Component {
/** /**
* Render exam widgets. * Render exam widgets.
* @param widget the corresponding widget object from the db * @param widget the corresponding widget object from the db
* @returns {Array} * @returns {Array} an array of react components to be displayed
*/ */
renderExamWidget = (widget) => { renderExamWidget = (widget) => {
if (this.props.finalized) return [] if (this.props.finalized) return []
...@@ -405,15 +474,9 @@ class ExamEditor extends React.Component { ...@@ -405,15 +474,9 @@ class ExamEditor extends React.Component {
x: { $set: Math.round(data.x) }, x: { $set: Math.round(data.x) },
y: { $set: Math.round(data.y) } y: { $set: Math.round(data.y) }
}) })
api.patch('widgets/' + widget.id, { this.updateWidgetDB({
x: Math.round(data.x), x: Math.round(data.x),
y: Math.round(data.y) y: Math.round(data.y)
}).then(() => {
// ok
}).catch(err => {
console.log(err)
// update to try and get a consistent state
this.props.updateExam()
}) })
}} }}
> >
...@@ -431,7 +494,7 @@ class ExamEditor extends React.Component { ...@@ -431,7 +494,7 @@ class ExamEditor extends React.Component {
/** /**
* Render all the widgets by calling the right rendering function for each widget type * Render all the widgets by calling the right rendering function for each widget type
* @returns {Array} * @returns {Array} containing all widgets components to be displayed
*/ */
renderWidgets = () => { renderWidgets = () => {
// Only render when numPage is set // Only render when numPage is set
......
...@@ -49,8 +49,7 @@ def get_cb_data_for_exam(exam): ...@@ -49,8 +49,7 @@ def get_cb_data_for_exam(exam):
cb_data = [] cb_data = []
for problem in exam.problems: for problem in exam.problems:
page = problem.widget.page page = problem.widget.page
if page: cb_data += [(cb.x, cb.y, page, cb.label) for cb in problem.mc_options]
cb_data += [(cb.x, cb.y, page, cb.label) for cb in problem.mc_options]
return cb_data return cb_data
......
...@@ -162,25 +162,27 @@ def generate_checkbox(canvas, x, y, label): ...@@ -162,25 +162,27 @@ def generate_checkbox(canvas, x, y, label):
canvas : reportlab canvas object canvas : reportlab canvas object
x : int x : int
the x coordinate of the top left corner of the box in pixels the x coordinate of the top left corner of the box in points (pt)
y : int y : int
the y coordinate of the top left corner of the box in pixels the y coordinate of the top left corner of the box in points (pt)
label: str label: str
A string representing the label that is drawn on top of the box, will only take the first character A string representing the label that is drawn on top of the box, will only take the first character
""" """
fontsize = 11 # Size of font fontsize = 11 # Size of font
margin = 5 # Margin between elements and sides margin = 5 # Margin between elements and sides
markboxsize = fontsize - 2 # Size of student number boxes markboxsize = fontsize - 2 # Size of checkboxes boxes
x_label = x + 1 x_label = x + 1 # location of the label
y_label = y + margin + fontsize y_label = y + margin # remove fontsize from the y label since we draw from the bottom left up
box_y = y - markboxsize # remove the markboxsize because the y is the coord of the top
# and reportlab prints from the bottom
# check that there is a label to print # check that there is a label to print
if (label and not (len(label) == 0)): if (label and not (len(label) == 0)):
canvas.setFont('Helvetica', fontsize) canvas.setFont('Helvetica', fontsize)
canvas.drawString(x_label, y_label, label[0]) canvas.drawString(x_label, y_label, label[0])
canvas.rect(x, y, markboxsize, markboxsize) canvas.rect(x, box_y, markboxsize, markboxsize)
def generate_datamatrix(exam_id, page_num, copy_num): def generate_datamatrix(exam_id, page_num, copy_num):
...@@ -273,6 +275,8 @@ def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x, ...@@ -273,6 +275,8 @@ def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x,
index = 0 index = 0
max_index = len(cb_data) max_index = len(cb_data)
cb_data = sorted(cb_data, key=lambda tup: tup[2]) cb_data = sorted(cb_data, key=lambda tup: tup[2])
# invert the y axis
cb_data = [(cb[0], pagesize[1] - cb[1], cb[2], cb[3]) for cb in cb_data]
else: else:
index = 0 index = 0
max_index = 0 max_index = 0
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment