diff --git a/client/components/PaneMCQ.jsx b/client/components/PaneMCQ.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c77b1cc47063620eb9255b186a229aabfb052617
--- /dev/null
+++ b/client/components/PaneMCQ.jsx
@@ -0,0 +1,146 @@
+import React from 'react'
+
+/**
+ * PanelMCQ is a component that allows the user to generate mcq options
+ */
+class PanelMCQ extends React.Component {
+  constructor (props) {
+    super(props)
+    this.onChangeNPA = this.onChangeNPA.bind(this)
+    this.onChangeLabelType = this.onChangeLabelType.bind(this)
+    this.generateLabels = this.generateLabels.bind(this)
+    this.state = {
+      chosenLabelType: 0,
+      nrPossibleAnswers: 2,
+      labelTypes: ['None', 'True/False', 'A, B, C ...', '1, 2, 3 ...']
+    }
+  }
+
+  // this function is called when the input is changed for the number of possible answers
+  onChangeNPA (e) {
+    let value = parseInt(e.target.value)
+    if (!isNaN(value)) {
+      if (this.state.chosenLabelType === 1) {
+        value = 2
+      }
+      this.setState({
+        nrPossibleAnswers: value
+      })
+    }
+  }
+
+  // this function is called when the input is changed for the desired label type
+  onChangeLabelType (e) {
+    let value = parseInt(e.target.value)
+    if (!isNaN(value)) {
+      this.setState({
+        chosenLabelType: value
+      })
+      if (parseInt(value) === 1) {
+        this.setState({
+          nrPossibleAnswers: 2
+        })
+      }
+    }
+  }
+
+  /**
+   * This function generates an array with the labels for each option
+   * @param nrLabels the number of options that need to be generated
+   * @returns {any[]|string[]|number[]}
+   */
+  generateLabels (nrLabels) {
+    let type = this.state.chosenLabelType
+
+    switch (type) {
+      case 1:
+        return ['T', 'F']
+      case 2:
+        return Array.from(Array(nrLabels).keys()).map((e) => String.fromCharCode(e + 65))
+      case 3:
+        return Array.from(Array(nrLabels).keys()).map(e => e + 1)
+      default:
+        return Array(nrLabels).fill(' ')
+    }
+  }
+
+  /**
+   * This function renders the panel with the inputs for generating multiple choice options
+   * @returns the react component containing the mcq panel
+   */
+  render () {
+    return (
+      <nav className='panel'>
+        <p className='panel-heading'>
+          Multiple Choice Question
+        </p>
+        <div className='panel-block'>
+          <div className='field'>
+            <React.Fragment>
+              <label className='label'>Number possible answers</label>
+              <div className='control'>
+                {(function () {
+                  var optionList = []
+                  for (var i = 1; i <= this.props.totalNrAnswers; i++) {
+                    const optionElement = <option key={i} value={String(i)}>{i}</option>
+                    optionList.push(optionElement)
+                  }
+                  return (<div className='select is-hovered is-fullwidth'>
+                    <select value={this.state.nrPossibleAnswers} onChange={this.onChangeNPA}>{optionList}</select>
+                  </div>)
+                }.bind(this)())}
+              </div>
+            </React.Fragment>
+          </div>
+        </div>
+        <div className='panel-block'>
+          <div className='field'>
+            <React.Fragment>
+              <label className='label'>Answer boxes labels</label>
+              <div className='control'>
+                <div className='select is-hovered is-fullwidth'>
+                  {(function () {
+                    var optionList = this.state.labelTypes.map(
+                      (label, i) => <option key={i} value={String(i)}>{label}</option>
+                    )
+                    return (
+                      <div className='select is-hovered is-fullwidth'>
+                        <select value={this.state.chosenLabelType} onChange={this.onChangeLabelType}>
+                          {optionList}
+                        </select>
+                      </div>
+                    )
+                  }.bind(this)())}
+                </div>
+              </div>
+            </React.Fragment>
+          </div>
+        </div>
+        <div className='panel-block field is-grouped'>
+          <button
+            disabled={this.props.disabledGenerateBoxes}
+            className='button is-link is-fullwidth'
+            onClick={() => {
+              let npa = this.state.nrPossibleAnswers
+              let labels = this.generateLabels(npa)
+              this.props.onGenerateBoxesClick(labels)
+            }}
+          >
+            Generate
+          </button>
+          <button
+            disabled={this.props.disabledDeleteBoxes}
+            className='button is-danger is-fullwidth'
+            onClick={() => {
+              this.props.onDeleteBoxesClick()
+            }}
+          >
+            Delete
+          </button>
+        </div>
+      </nav>
+    )
+  }
+}
+
+export default PanelMCQ
diff --git a/client/components/answer_box.png b/client/components/answer_box.png
new file mode 100644
index 0000000000000000000000000000000000000000..eafc3d30a9f99f21693f9075b9d446e43ce933fc
Binary files /dev/null and b/client/components/answer_box.png differ
diff --git a/client/views/Exam.css b/client/views/Exam.css
index f126f590b4b1ecb24c75893ea13582ced0d185a4..e70724da849022d617945ccebe204ea143ad9fea 100644
--- a/client/views/Exam.css
+++ b/client/views/Exam.css
@@ -1,3 +1,33 @@
+:root {
+    --option-width:20px;
+    --label-font-size:14px;
+}
+
+div.mcq-widget {
+    display:inline-flex;
+}
+
+div.mcq-option {
+    display: block;
+    width: var(--option-width);
+    padding:2px;
+    box-sizing: content-box;
+    height: auto;
+}
+
+div.mcq-option div.mcq-option-label {
+    display:block;
+    font-family: Arial, Helvetica, sans-serif;
+    font-size: var(--label-font-size);
+    text-align: center;
+}
+
+div.mcq-option img.mcq-box {
+    display: block;
+    margin:auto;
+}
+
+
 .editor-content {
   background-color: #ddd;
   border-radius: 10px
diff --git a/client/views/Exam.jsx b/client/views/Exam.jsx
index 4f48dfae59bd06c183e41f997c54f471f3ba299b..3ccbc0087586e63a07fe50a09b7b2900dded0730 100644
--- a/client/views/Exam.jsx
+++ b/client/views/Exam.jsx
@@ -6,6 +6,7 @@ import Hero from '../components/Hero.jsx'
 import './Exam.css'
 import GeneratedExamPreview from '../components/GeneratedExamPreview.jsx'
 import PanelGenerate from '../components/PanelGenerate.jsx'
+import PanelMCQ from '../components/PaneMCQ.jsx'
 import ExamEditor from './ExamEditor.jsx'
 import update from 'immutability-helper'
 import ExamFinalizeMarkdown from './ExamFinalize.md'
@@ -23,7 +24,9 @@ class Exams extends React.Component {
     widgets: [],
     previewing: false,
     deletingExam: false,
-    deletingWidget: false
+    deletingWidget: false,
+    deletingMCWidget: false,
+    showPanelMCQ: false
   }
 
   static getDerivedStateFromProps = (newProps, prevState) => {
@@ -38,7 +41,13 @@ class Exams extends React.Component {
             id: problem.id,
             page: problem.page,
             name: problem.name,
-            graded: problem.graded
+            graded: problem.graded,
+            mc_options: problem.mc_options.map((option) => {
+              option.widget.x -= 7
+              option.widget.y -= 21
+              return option
+            }),
+            isMCQ: problem.mc_options && problem.mc_options.length !== 0 // is the problem a mc question - used to display PanelMCQ
           }
         }
       })
@@ -102,6 +111,19 @@ class Exams extends React.Component {
       }))
   }
 
+  createNewWidget = (widgetData) => {
+    this.setState((prevState) => {
+      return {
+        selectedWidgetId: widgetData.id,
+        widgets: update(prevState.widgets, {
+          [widgetData.id]: {
+            $set: widgetData
+          }
+        })
+      }
+    })
+  }
+
   deleteWidget = (widgetId) => {
     const widget = this.state.widgets[widgetId]
     if (widget) {
@@ -166,17 +188,10 @@ class Exams extends React.Component {
               selectedWidgetId: widgetId
             })
           }}
-          createNewWidget={(widgetData) => {
-            this.setState((prevState) => {
-              return {
-                selectedWidgetId: widgetData.id,
-                widgets: update(prevState.widgets, {
-                  [widgetData.id]: {
-                    $set: widgetData
-                  }
-                })
-              }
-            })
+          createNewWidget={this.createNewWidget}
+          updateMCWidgetPosition={this.updateMCWidgetPosition}
+          updateExam={() => {
+            this.props.updateExam(this.props.examID)
           }}
         />
       )
@@ -236,6 +251,143 @@ class Exams extends React.Component {
     })
   }
 
+  /**
+   * This function deletes the mc options coupled to a problem.
+   */
+  deleteMCWidget = () => {
+    const widget = this.state.widgets[this.state.selectedWidgetId]
+    const options = widget.problem.mc_options
+    if (options.length > 0) {
+      options.forEach((option) => {
+        api.del('mult-choice/' + option.id)
+          .catch(err => {
+            console.log(err)
+            err.json().then(res => {
+              this.setState({
+                deletingMCWidget: false
+              })
+              Notification.error('Could not delete multiple choice option' +
+                (res.message ? ': ' + res.message : ''))
+              // update to try and get a consistent state
+              this.props.updateExam(this.props.examID)
+            })
+          })
+      })
+
+      // remove the mc options from the state
+      // note that his can happen before they are removed in the DB due to async calls
+      this.setState((prevState) => {
+        return {
+          widgets: update(prevState.widgets, {
+            [widget.id]: {
+              problem: {
+                mc_options: {
+                  $set: []
+                }
+              }
+            }
+          }),
+          deletingMCWidget: false
+        }
+      })
+    }
+  }
+
+  /**
+   * This method creates a widget object and adds it to the corresponding problem
+   * @param problemWidget The widget the mc option belongs to
+   * @param data the mc option
+   */
+  createNewMCOWidget = (problemWidget, data) => {
+    this.setState((prevState) => {
+      return {
+        widgets: update(prevState.widgets, {
+          [this.state.selectedWidgetId]: {
+            problem: {
+              mc_options: {
+                $push: [data]
+              }
+            }
+          }
+        })
+      }
+    })
+  }
+
+  /**
+   * This method is called when the mcq widget is moved. The positions of the options are stored separately and they
+  * all need to be updated
+   * @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)
+   */
+  updateMCWidgetPosition = (widget, data) => {
+    let newMCO = widget.problem.mc_options.map((option, i) => {
+      return {
+        'widget': {
+          'x': {
+            $set: data.x + i * 24
+          },
+          'y': {
+            // each mc option needs to be positioned next to the previous option and should not overlap it
+            $set: data.y
+          }
+        }
+      }
+    })
+
+    // update the state with the new locations
+    this.setState(prevState => ({
+      widgets: update(prevState.widgets, {
+        [widget.id]: {
+          'problem': {
+            'mc_options': newMCO
+          }
+        }
+      })
+    }))
+  }
+
+  /**
+   * This method generates MC options by making the right calls to the api and creating
+   * the widget object in the mc_options array of the corresponding problem.
+   * @param problemWidget the problem widget the mc options belong to
+   * @param labels the labels for the options
+   * @param index the index in the labels array (the function is recusive, this index is increased)
+   * @param xPos x position of the current option
+   * @param yPos y position of the current option
+   */
+  generateAnswerBoxes = (problemWidget, labels, index, xPos, yPos) => {
+    if (labels.length === index) return
+
+    let data = {
+      'label': labels[index],
+      'problem_id': problemWidget.problem.id,
+      'feedback_id': null,
+      'widget': {
+        'name': 'mc_option_' + labels[index],
+        'x': xPos + 7,
+        'y': yPos + 21,
+        'type': 'mcq_widget'
+      }
+    }
+
+    const formData = new window.FormData()
+    formData.append('name', data.widget.name)
+    formData.append('x', data.widget.x)
+    formData.append('y', data.widget.y)
+    formData.append('problem_id', data.problem_id)
+    formData.append('label', data.label)
+    api.put('mult-choice/', formData).then(result => {
+      data.id = result.mult_choice_id
+      data.widget.x -= 7
+      data.widget.y -= 21
+      this.createNewMCOWidget(problemWidget, data)
+      this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + 24, yPos)
+    }).catch(err => {
+      console.log(err)
+    })
+  }
+
   SidePanel = (props) => {
     const selectedWidgetId = this.state.selectedWidgetId
     let selectedWidget = selectedWidgetId && this.state.widgets[selectedWidgetId]
@@ -243,11 +395,16 @@ class Exams extends React.Component {
     let widgetEditDisabled = this.state.previewing || !problem
     let isGraded = problem && problem.graded
     let widgetDeleteDisabled = widgetEditDisabled || isGraded
+    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 isMCQ = (problem && problem.isMCQ) || false
 
     return (
       <React.Fragment>
         <this.PanelEdit
           disabledEdit={widgetEditDisabled}
+          disableIsMCQ={widgetEditDisabled || containsMCOptions}
           disabledDelete={widgetDeleteDisabled}
           onDeleteClick={() => {
             this.setState({deletingWidget: true})
@@ -268,7 +425,42 @@ class Exams extends React.Component {
             }))
           }}
           saveProblemName={this.saveProblemName}
+          isMCQProblem={isMCQ}
+          onMCQChange={
+            (checked) => {
+              this.setState(prevState => ({
+                changedWidgetId: selectedWidgetId,
+                widgets: update(prevState.widgets, {
+                  [selectedWidgetId]: {
+                    problem: {
+                      isMCQ: {
+                        $set: checked
+                      }
+                    }
+                  }
+                })
+              }))
+            }
+          }
         />
+        { isMCQ ? (
+          <PanelMCQ
+            totalNrAnswers={totalNrAnswers}
+            disabledGenerateBoxes={containsMCOptions}
+            disabledDeleteBoxes={disabledDeleteBoxes}
+            problem={problem}
+            onGenerateBoxesClick={(labels) => {
+              let problemWidget = this.state.widgets[this.state.selectedWidgetId]
+              // position the new mc option widget inside the problem widget
+              let xPos = problemWidget.x + 2
+              let yPos = problemWidget.y + 2
+              this.generateAnswerBoxes(problemWidget, labels, 0, xPos, yPos)
+            }}
+            onDeleteBoxesClick={() => {
+              this.setState({deletingMCWidget: true})
+            }}
+          />
+        ) : null }
         <this.PanelExamActions />
       </React.Fragment>
     )
@@ -282,14 +474,18 @@ class Exams extends React.Component {
         <p className='panel-heading'>
           Problem details
         </p>
-        <div className='panel-block'>
-          <div className='field'>
-            {selectedWidgetId === null ? (
-              <p style={{margin: '0.625em 0', minHeight: '3em'}}>
+        {selectedWidgetId === null ? (
+          <div className='panel-block'>
+            <div className='field'>
+              <p style={{ margin: '0.625em 0', minHeight: '3em' }}>
                 To create a problem, draw a rectangle on the exam.
               </p>
-            ) : (
-              <React.Fragment>
+            </div>
+          </div>
+        ) : (
+          <React.Fragment>
+            <div className='panel-block'>
+              <div className='field'>
                 <label className='label'>Name</label>
                 <div className='control'>
                   <input
@@ -302,12 +498,24 @@ class Exams extends React.Component {
                     }}
                     onBlur={(e) => {
                       props.saveProblemName(e.target.value)
-                    }} />
+                    }}
+                  />
                 </div>
-              </React.Fragment>
-            )}
-          </div>
-        </div>
+              </div>
+            </div>
+            <div className='panel-block'>
+              <div className='field'>
+                <label className='label'>
+                  <input disabled={props.disableIsMCQ} type='checkbox' checked={props.isMCQProblem} onChange={
+                    (e) => {
+                      props.onMCQChange(e.target.checked)
+                    }} />
+                    Multiple choice question
+                </label>
+              </div>
+            </div>
+          </React.Fragment>
+        )}
         <div className='panel-block'>
           <button
             disabled={props.disabledDelete}
@@ -317,7 +525,6 @@ class Exams extends React.Component {
             Delete problem
           </button>
         </div>
-
       </nav>
     )
   }
@@ -453,6 +660,18 @@ class Exams extends React.Component {
         onCancel={() => this.setState({deletingWidget: false})}
         onConfirm={() => this.deleteWidget(this.state.selectedWidgetId)}
       />
+      <ConfirmationModal
+        active={this.state.deletingMCWidget && this.state.selectedWidgetId != null}
+        color='is-danger'
+        headerText={`Are you sure you want to delete the multiple choice options for problem "${
+          this.state.selectedWidgetId &&
+          this.state.widgets[this.state.selectedWidgetId] &&
+          this.state.widgets[this.state.selectedWidgetId].problem &&
+          this.state.widgets[this.state.selectedWidgetId].problem.name}"`}
+        confirmText='Delete multiple choice options'
+        onCancel={() => this.setState({deletingMCWidget: false})}
+        onConfirm={() => this.deleteMCWidget(this.state.selectedWidgetId)}
+      />
     </div>
   }
 }
diff --git a/client/views/ExamEditor.jsx b/client/views/ExamEditor.jsx
index 89cf1d8d163c9ace55510395de54147c9d694b96..311cadafb250703af69f983cae03a8e88438ea5e 100644
--- a/client/views/ExamEditor.jsx
+++ b/client/views/ExamEditor.jsx
@@ -8,6 +8,7 @@ import studentIdExampleImage from '../components/student_id_example.png'
 // FIXME!
 // eslint-disable-next-line import/no-webpack-loader-syntax
 import studentIdExampleImageSize from '!image-dimensions-loader!../components/student_id_example.png'
+import answerBoxImage from '../components/answer_box.png'
 import EmptyPDF from '../components/EmptyPDF.jsx'
 import PDFOverlay from '../components/PDFOverlay.jsx'
 
@@ -86,13 +87,16 @@ class ExamEditor extends React.Component {
       if (selectionBox.width >= this.props.problemMinWidth && selectionBox.height >= this.props.problemMinHeight) {
         const problemData = {
           name: 'New problem', // TODO: Name
-          page: this.props.page
+          page: this.props.page,
+          mc_options: [],
+          isMCQ: false
         }
         const widgetData = {
           x: Math.round(selectionBox.left),
           y: Math.round(selectionBox.top),
           width: Math.round(selectionBox.width),
-          height: Math.round(selectionBox.height)
+          height: Math.round(selectionBox.height),
+          type: 'problem_widget'
         }
         const formData = new window.FormData()
         formData.append('exam_id', this.props.examID)
@@ -169,131 +173,334 @@ class ExamEditor extends React.Component {
     }
   }
 
-  renderWidgets = () => {
-    // Only render when numPage is set
-    if (this.props.numPages !== null && this.props.widgets) {
-      const widgets = this.props.widgets.filter(widget => {
-        if (widget.name === 'student_id_widget' ||
-          widget.name === 'barcode_widget') {
-          return !this.props.finalized
-        } else if (widget.problem) {
-          return widget.problem.page === this.props.page
-        } else {
-          return true
+  /**
+   * This method is called when the position of a widget has changed. It informs the server about the relocation.
+   * @param widget the widget that was relocated
+   * @param data  the new location
+   */
+  updateWidgetPositionDB = (widget, data) => {
+    api.patch('widgets/' + widget.id, data).then(() => {
+      // ok
+    }).catch(err => {
+      console.log(err)
+      // update to try and get a consistent state
+      this.props.updateExam()
+    })
+  }
+
+  updateState = (widget, data) => {
+    this.props.updateMCWidgetPosition(widget, {
+      x: Math.round(data.x),
+      y: Math.round(data.y)
+    })
+  }
+
+  updateMCOPosition = (widget, data) => {
+    this.updateState(widget, data)
+
+    widget.problem.mc_options.forEach(
+      (option, i) => {
+        let newData = {
+          x: Math.round(data.x) + i * 24 + 7,
+          y: Math.round(data.y) + 21
         }
+        this.updateWidgetPositionDB(option, newData)
       })
+  }
+
+  /**
+   * This function renders a group of options into one draggable widget
+   * @returns {*}
+   */
+  renderMCWidget = (widget) => {
+    let width = 24 * widget.problem.mc_options.length
+    let height = 38
+    let enableResizing = false
+    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 (
+      <ResizeAndDrag
+        key={'widget_mc_' + widget.id}
+        bounds={'[data-key="widget_' + widget.id + '"]'}
+        minWidth={width}
+        minHeight={height}
+        enableResizing={{
+          bottom: enableResizing,
+          bottomLeft: enableResizing,
+          bottomRight: enableResizing,
+          left: enableResizing,
+          right: enableResizing,
+          top: enableResizing,
+          topLeft: enableResizing,
+          topRight: enableResizing
+        }}
+        position={{
+          x: xPos,
+          y: yPos
+        }}
+        size={{
+          width: width,
+          height: height
+        }}
+        onDragStart={() => {
+          this.props.selectWidget(widget.id)
+        }}
+        onDragStop={(e, data) => {
+          this.updateMCOPosition(widget, data)
+        }}
+      >
+        <div className={isSelected ? 'mcq-widget widget selected' : 'mcq-widget widget '}>
+          {widget.problem.mc_options.map((option) => {
+            return (
+              <div key={'widget_mco_' + option.id} className='mcq-option'>
+                <div className='mcq-option-label'>
+                  {option.label}
+                </div>
+                <img className='mcq-box' src={answerBoxImage} />
+              </div>
+            )
+          })}
+        </div>
+      </ResizeAndDrag>
+    )
+  }
+
+  /**
+   * Render problem widget and the mc options that correspond to the problem
+   * @param widget the corresponding widget object from the db
+   * @returns {Array}
+   */
+  renderProblemWidget = (widget) => {
+    // Only render when numPage is set
+    if (widget.problem.page !== this.props.page) return []
+
+    let enableResizing = true
+    const isSelected = widget.id === this.props.selectedWidgetId
+    let minWidth = this.props.problemMinWidth
+    let minHeight = this.props.problemMinHeight
+    let elementList = [(
+      <ResizeAndDrag
+        key={'widget_' + widget.id}
+        data-key={'widget_' + widget.id}
+        bounds='parent'
+        minWidth={minWidth}
+        minHeight={minHeight}
+        enableResizing={{
+          bottom: enableResizing,
+          bottomLeft: enableResizing,
+          bottomRight: enableResizing,
+          left: enableResizing,
+          right: enableResizing,
+          top: enableResizing,
+          topLeft: enableResizing,
+          topRight: enableResizing
+        }}
+        position={{
+          x: widget.x,
+          y: widget.y
+        }}
+        size={{
+          width: widget.width,
+          height: widget.height
+        }}
+        onResize={(e, direction, ref, delta, position) => {
+          this.props.updateWidget(widget.id, {
+            width: { $set: ref.offsetWidth },
+            height: { $set: ref.offsetHeight },
+            x: { $set: Math.round(position.x) },
+            y: { $set: Math.round(position.y) }
+          })
+        }}
+        onResizeStop={(e, direction, ref, delta, position) => {
+          api.patch('widgets/' + widget.id, {
+            x: Math.round(position.x),
+            y: Math.round(position.y),
+            width: ref.offsetWidth,
+            height: ref.offsetHeight
+          }).then(() => {
+            // ok
+          }).catch(err => {
+            console.log(err)
+            // update to try and get a consistent state
+            this.props.updateExam()
+          })
+        }}
+        onDragStart={() => {
+          this.props.selectWidget(widget.id)
+        }}
+        onDrag={(e, data) => {
+          if (widget.problem.mc_options.length > 0) {
+            let xPos = widget.problem.mc_options[0].widget.x
+            let yPos = widget.problem.mc_options[0].widget.y
+            let width = 24 * widget.problem.mc_options.length
+            let height = 38
+
+            if (xPos < data.x) {
+              xPos = data.x
+            } else if (xPos + width > data.x + widget.width) {
+              xPos = data.x + widget.width - width
+            }
 
-      let minWidth
-      let minHeight
-      let view
-      let enableResizing
-      return widgets.map((widget) => {
-        const isSelected = widget.id === this.props.selectedWidgetId
-
-        if (widget.problem) {
-          minWidth = this.props.problemMinWidth
-          minHeight = this.props.problemMinHeight
-          view = (
-            <div
-              className={isSelected ? 'widget selected' : 'widget'}
-            />
-          )
-          enableResizing = true
-        } else {
-          let image
-          if (widget.name === 'barcode_widget') {
-            minWidth = barcodeExampleImageSize.width
-            minHeight = barcodeExampleImageSize.height
-            image = barcodeExampleImage
-          } else if (this.props.page === 0 && widget.name === 'student_id_widget') {
-            minWidth = studentIdExampleImageSize.width
-            minHeight = studentIdExampleImageSize.height
-            image = studentIdExampleImage
-          } else {
-            return null
+            if (yPos < data.y) {
+              yPos = data.y
+            } else if (yPos + height > data.y + widget.height) {
+              yPos = data.y + widget.height - height
+            }
+
+            this.updateState(widget, { x: xPos, y: yPos })
           }
-          view = (
-            <div
-              className={isSelected ? 'widget selected' : 'widget'}
-              style={{
-                boxSizing: 'content-box',
-                backgroundImage: 'url(' + image + ')',
-                backgroundRepeat: 'no-repeat'
-              }}
-            />
-          )
-          enableResizing = false
+        }}
+        onDragStop={(e, data) => {
+          this.props.updateWidget(widget.id, {
+            x: { $set: Math.round(data.x) },
+            y: { $set: Math.round(data.y) }
+          })
+          api.patch('widgets/' + widget.id, {
+            x: Math.round(data.x),
+            y: Math.round(data.y)
+          }).then(() => {
+            if (widget.problem.mc_options.length > 0) {
+              let xPos = widget.problem.mc_options[0].widget.x
+              let yPos = widget.problem.mc_options[0].widget.y
+              let width = 24 * widget.problem.mc_options.length
+              let height = 38
+
+              if (xPos < data.x) {
+                xPos = data.x
+              } else if (xPos + width > data.x + widget.width) {
+                xPos = data.x + widget.width - width
+              }
+
+              if (yPos < data.y) {
+                yPos = data.y
+              } else if (yPos + height > data.y + widget.height) {
+                yPos = data.y + widget.height - height
+              }
+
+              this.updateMCOPosition(widget, { x: xPos, y: yPos })
+            }
+          }).catch(err => {
+            console.log(err)
+            // update to try and get a consistent state
+            this.props.updateExam()
+          })
+        }}
+      >
+        <div
+          className={isSelected ? 'widget selected' : 'widget'}
+        />
+      </ResizeAndDrag>
+    )]
+
+    // depending on the rendering option, render the mc_options separately or in a single widget
+    if (widget.problem.mc_options.length > 0 && !this.props.finalized) {
+      elementList.push(this.renderMCWidget(widget))
+    }
+
+    return elementList
+  }
+
+  /**
+   * Render exam widgets.
+   * @param widget the corresponding widget object from the db
+   * @returns {Array}
+   */
+  renderExamWidget = (widget) => {
+    if (this.props.finalized) return []
+
+    let minWidth, minHeight
+    let enableResizing = false
+    const isSelected = widget.id === this.props.selectedWidgetId
+    let image
+    if (widget.name === 'barcode_widget') {
+      minWidth = barcodeExampleImageSize.width
+      minHeight = barcodeExampleImageSize.height
+      image = barcodeExampleImage
+    } else if (this.props.page === 0 && widget.name === 'student_id_widget') {
+      minWidth = studentIdExampleImageSize.width
+      minHeight = studentIdExampleImageSize.height
+      image = studentIdExampleImage
+    } else {
+      return []
+    }
+
+    return [(
+      <ResizeAndDrag
+        key={'widget_' + widget.id}
+        bounds='parent'
+        minWidth={minWidth}
+        minHeight={minHeight}
+        enableResizing={{
+          bottom: enableResizing,
+          bottomLeft: enableResizing,
+          bottomRight: enableResizing,
+          left: enableResizing,
+          right: enableResizing,
+          top: enableResizing,
+          topLeft: enableResizing,
+          topRight: enableResizing
+        }}
+        position={{
+          x: widget.x,
+          y: widget.y
+        }}
+        size={{
+          width: widget.width,
+          height: widget.height
+        }}
+        onDragStart={() => {
+          this.props.selectWidget(widget.id)
+        }}
+        onDragStop={(e, data) => {
+          this.props.updateWidget(widget.id, {
+            x: { $set: Math.round(data.x) },
+            y: { $set: Math.round(data.y) }
+          })
+          api.patch('widgets/' + widget.id, {
+            x: Math.round(data.x),
+            y: Math.round(data.y)
+          }).then(() => {
+            // ok
+          }).catch(err => {
+            console.log(err)
+            // update to try and get a consistent state
+            this.props.updateExam()
+          })
+        }}
+      >
+        <div
+          className={isSelected ? 'widget selected' : 'widget'}
+          style={{
+            boxSizing: 'content-box',
+            backgroundImage: 'url(' + image + ')',
+            backgroundRepeat: 'no-repeat'
+          }}
+        />
+      </ResizeAndDrag>
+    )]
+  }
+
+  /**
+   * Render all the widgets by calling the right rendering function for each widget type
+   * @returns {Array}
+   */
+  renderWidgets = () => {
+    // Only render when numPage is set
+    if (this.props.numPages !== null && this.props.widgets) {
+      let widgets = this.props.widgets
+      let elementList = []
+
+      widgets.forEach((widget) => {
+        if (widget.type === 'exam_widget') {
+          elementList = elementList.concat(this.renderExamWidget(widget))
+        } else if (widget.type === 'problem_widget') {
+          elementList = elementList.concat(this.renderProblemWidget(widget))
         }
-        return (
-          <ResizeAndDrag
-            key={'widget_' + widget.id}
-            bounds='parent'
-            minWidth={minWidth}
-            minHeight={minHeight}
-            enableResizing={{
-              bottom: enableResizing,
-              bottomLeft: enableResizing,
-              bottomRight: enableResizing,
-              left: enableResizing,
-              right: enableResizing,
-              top: enableResizing,
-              topLeft: enableResizing,
-              topRight: enableResizing
-            }}
-            position={{
-              x: widget.x,
-              y: widget.y
-            }}
-            size={{
-              width: widget.width,
-              height: widget.height
-            }}
-            onResize={(e, direction, ref, delta, position) => {
-              this.props.updateWidget(widget.id, {
-                width: { $set: ref.offsetWidth },
-                height: { $set: ref.offsetHeight },
-                x: { $set: Math.round(position.x) },
-                y: { $set: Math.round(position.y) }
-              })
-            }}
-            onResizeStop={(e, direction, ref, delta, position) => {
-              api.patch('widgets/' + widget.id, {
-                x: Math.round(position.x),
-                y: Math.round(position.y),
-                width: ref.offsetWidth,
-                height: ref.offsetHeight
-              }).then(() => {
-                // ok
-              }).catch(err => {
-                console.log(err)
-                // update to try and get a consistent state
-                this.updateExam()
-              })
-            }}
-            onDragStart={() => {
-              this.props.selectWidget(widget.id)
-            }}
-            onDragStop={(e, data) => {
-              this.props.updateWidget(widget.id, {
-                x: { $set: Math.round(data.x) },
-                y: { $set: Math.round(data.y) }
-              })
-              api.patch('widgets/' + widget.id, {
-                x: Math.round(data.x),
-                y: Math.round(data.y)
-              }).then(() => {
-                // ok
-              }).catch(err => {
-                console.log(err)
-                // update to try and get a consistent state
-                this.updateExam()
-              })
-            }}
-          >
-            {view}
-          </ResizeAndDrag>
-        )
       })
+
+      return elementList
     }
   }
 
diff --git a/data/course.sqlite b/data/course.sqlite
new file mode 100644
index 0000000000000000000000000000000000000000..42369d0718bc1a16a99c196ae7dcd845a0a3ab30
Binary files /dev/null and b/data/course.sqlite differ
diff --git a/migrations/versions/b46a2994605b_.py b/migrations/versions/b46a2994605b_.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c7e740429ee92a25f3feef629ead82d9cf409d1
--- /dev/null
+++ b/migrations/versions/b46a2994605b_.py
@@ -0,0 +1,35 @@
+"""empty message
+
+Revision ID: b46a2994605b
+Revises: 4204f4a83863
+Create Date: 2019-05-15 15:41:56.615076
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b46a2994605b'
+down_revision = '4204f4a83863'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('mc_option',
+                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+                    sa.Column('label', sa.String(), nullable=True),
+                    sa.Column('feedback_id', sa.Integer(), nullable=False),
+                    sa.ForeignKeyConstraint(['feedback_id'], ['feedback_option.id'], ),
+                    sa.ForeignKeyConstraint(['id'], ['widget.id'], ),
+                    sa.PrimaryKeyConstraint('id')
+                    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('mc_option')
+    # ### end Alembic commands ###
diff --git a/tests/data/.gitignore b/tests/data/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..f0aeb239c4e97d2966b0e58cf21fd8bac24e53cb
--- /dev/null
+++ b/tests/data/.gitignore
@@ -0,0 +1 @@
+submissions
\ No newline at end of file
diff --git a/tests/data/checkboxes/scanned_page.jpg b/tests/data/checkboxes/scanned_page.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9bcdd3c91e856bf3c474b13e600e1b6f5db90930
Binary files /dev/null and b/tests/data/checkboxes/scanned_page.jpg differ
diff --git a/tests/data/cornermarkers/a4-3-markers.png b/tests/data/cornermarkers/a4-3-markers.png
new file mode 100644
index 0000000000000000000000000000000000000000..32bdd11a2fbf87fed48625c3a12f0e609e2088a6
Binary files /dev/null and b/tests/data/cornermarkers/a4-3-markers.png differ
diff --git a/tests/data/cornermarkers/a4-rotated-3-markers.png b/tests/data/cornermarkers/a4-rotated-3-markers.png
new file mode 100644
index 0000000000000000000000000000000000000000..d32cbacac4d4a2d8a327736ce8967623f8aca22d
Binary files /dev/null and b/tests/data/cornermarkers/a4-rotated-3-markers.png differ
diff --git a/tests/data/cornermarkers/a4-rotated.png b/tests/data/cornermarkers/a4-rotated.png
new file mode 100644
index 0000000000000000000000000000000000000000..8dbf8630a73b83a71dcc36eb687933632657f5aa
Binary files /dev/null and b/tests/data/cornermarkers/a4-rotated.png differ
diff --git a/tests/test_database.py b/tests/test_database.py
index c7b99fc983cb80834c95eaf1496708658f80c851..c890a1880b17706ca7d80f152ba80ab788cab21c 100644
--- a/tests/test_database.py
+++ b/tests/test_database.py
@@ -13,11 +13,11 @@ def test_exam_generate_token_length_uppercase(duplicate_count, monkeypatch):
             self.duplicates = duplicate_count + 1
 
         def filter(self, *args):
-                return self
+            return self
 
         def first(self):
-                self.duplicates -= 1
-                return None if self.duplicates else True
+            self.duplicates -= 1
+            return None if self.duplicates else True
 
     app = Flask(__name__, static_folder=None)
     app.config.update(
diff --git a/tests/test_pdf_generation.py b/tests/test_pdf_generation.py
index 1793442bc672ab1abad1219e8af0ae9c8cf40d0c..4324f34af928bef75447a5ff4471f048db16f0e3 100644
--- a/tests/test_pdf_generation.py
+++ b/tests/test_pdf_generation.py
@@ -106,6 +106,19 @@ def test_generate_pdfs_num_files(datadir, tmpdir):
     assert len(tmpdir.listdir()) == num_copies
 
 
+@pytest.mark.parametrize('checkboxes', [[(300, 100, 1, 'c'), (500, 50, 0, 'd'), (500, 500, 0, 'a'), (250, 200, 1, 'b')],
+                         [], [(250, 100, 0, None)]])
+def test_generate_checkboxes(datadir, tmpdir, checkboxes):
+    blank_pdf = os.path.join(datadir, 'blank-a4-2pages.pdf')
+
+    num_copies = 1
+    copy_nums = range(num_copies)
+    paths = map(lambda copy_num: os.path.join(tmpdir, f'{copy_num}.pdf'), copy_nums)
+    pdf_generation.generate_pdfs(blank_pdf, 'ABCDEFGHIJKL', copy_nums, paths, 25, 270, 150, 270, checkboxes)
+
+    assert len(tmpdir.listdir()) == num_copies
+
+
 @pytest.mark.parametrize('name', ['a4', 'square'], ids=['a4', 'square'])
 def test_join_pdfs(mock_generate_datamatrix, mock_generate_id_grid,
                    datadir, tmpdir, name):
diff --git a/tests/test_pregrader.py b/tests/test_pregrader.py
new file mode 100644
index 0000000000000000000000000000000000000000..895adb3e71b6543f8fc9abdad0375c3221c18780
--- /dev/null
+++ b/tests/test_pregrader.py
@@ -0,0 +1,50 @@
+import os
+import pytest
+from PIL import Image
+import numpy as np
+from zesje import pregrader
+from zesje import scans
+from zesje import images
+
+directory_name = "checkboxes"
+
+
+@pytest.fixture
+def scanned_image(datadir):
+    image_filename = os.path.join(datadir, directory_name, "scanned_page.jpg")
+    image = Image.open(image_filename)
+    image = np.array(image)
+    return image
+
+
+@pytest.fixture
+def scanned_image_keypoints(scanned_image):
+    corner_markers = scans.find_corner_marker_keypoints(scanned_image)
+    fixed_corner_keypoints = images.fix_corner_markers(corner_markers, scanned_image.shape)
+    return fixed_corner_keypoints
+
+
+@pytest.mark.parametrize('box_coords, result', [((346, 479), True), ((370, 479), False), ((393, 479), True),
+                                                ((416, 479), True), ((439, 479), True), ((155, 562), True)],
+                         ids=["1 filled", "2 empty", "3 marked with line", "4 completely filled",
+                              "5 marked with an x", "e marked with a cirle inside"])
+def test_ideal_crops(box_coords, result, scanned_image_keypoints, scanned_image):
+    assert pregrader.box_is_filled(box_coords, scanned_image, scanned_image_keypoints[0]) == result
+
+
+@pytest.mark.parametrize('box_coords, result', [((341, 471), True), ((352, 482), True), ((448, 482), True),
+                                                ((423, 474), True), ((460, 475), False), ((477, 474), True),
+                                                ((87, 548), False)],
+                         ids=["1 filled bottom right", "1 filled top left", "5 filled with a bit of 6",
+                              "4 fully filled with the label", "6 empty with label",
+                              "7 partially  cropped, filled and a part of 6", "B empty with cb at the bottom"])
+def test_shifted_crops(box_coords, result, scanned_image_keypoints, scanned_image):
+    assert pregrader.box_is_filled(box_coords, scanned_image, scanned_image_keypoints[0]) == result
+
+
+@pytest.mark.parametrize('box_coords, result', [((60, 562), True), ((107, 562), True),
+                                                ((131, 562), False)],
+                         ids=["A filled with trailing letter", "C filled with letters close",
+                              "D blank with trailing letter"])
+def test_trailing_text(box_coords, result, scanned_image_keypoints, scanned_image):
+    assert pregrader.box_is_filled(box_coords, scanned_image, scanned_image_keypoints[0]) == result
diff --git a/tests/test_three_corners.py b/tests/test_three_corners.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e3fb938e911061e2a5e741c619f9d86e7454a00
--- /dev/null
+++ b/tests/test_three_corners.py
@@ -0,0 +1,55 @@
+import cv2
+import os
+import numpy as np
+
+from zesje.images import fix_corner_markers
+from zesje.scans import find_corner_marker_keypoints
+
+
+def test_three_straight_corners_1():
+    shape = (240, 200, 3)
+    corner_markers = [(50, 50), (120, 50), (50, 200)]
+
+    corner_markers = fix_corner_markers(corner_markers, shape)
+
+    assert (120, 200) in corner_markers
+
+
+def test_three_straight_corners_2():
+    shape = (240, 200, 3)
+    corner_markers = [(120, 50), (50, 200), (120, 200)]
+
+    corner_markers = fix_corner_markers(corner_markers, shape)
+
+    assert (50, 50) in corner_markers
+
+
+def test_pdf(datadir):
+    # Max deviation of inferred corner marker and actual location
+    epsilon = 2
+
+    # Scan rotated image with 4 corner markers
+    image_filename1 = 'a4-rotated.png'
+    image_path = os.path.join(datadir, 'cornermarkers', image_filename1)
+    page_img = cv2.imread(image_path)
+
+    corners1 = find_corner_marker_keypoints(page_img)
+
+    # Scan the same image with 3 corner markers
+    image_filename2 = 'a4-rotated-3-markers.png'
+    image_path = os.path.join(datadir, 'cornermarkers', image_filename2)
+    page_img = cv2.imread(image_path)
+
+    corners2 = find_corner_marker_keypoints(page_img)
+
+    # Get marker that was removed
+    diff = [corner for corner in corners1 if corner not in corners2]
+    diff_marker = min(diff)
+
+    fixed_corners2 = fix_corner_markers(corners2, page_img.shape)
+    added_marker = [corner for corner in fixed_corners2 if corner not in corners1][0]
+
+    # Check if 'inferred' corner marker is not too far away
+    dist = np.linalg.norm(np.subtract(added_marker, diff_marker))
+
+    assert dist < epsilon
diff --git a/zesje/api/__init__.py b/zesje/api/__init__.py
index 58efb2732556d0daef199abd46a211f7f253fb4f..17dfe1554eee0c8752ef1e0f5e71b713d8d150f1 100644
--- a/zesje/api/__init__.py
+++ b/zesje/api/__init__.py
@@ -11,6 +11,8 @@ from .feedback import Feedback
 from .solutions import Solutions
 from .widgets import Widgets
 from .emails import EmailTemplate, RenderedEmailTemplate, Email
+from .mult_choice import MultipleChoice
+
 from . import signature
 from . import images
 from . import summary_plot
@@ -48,6 +50,9 @@ api.add_resource(RenderedEmailTemplate,
 api.add_resource(Email,
                  '/email/<int:exam_id>',
                  '/email/<int:exam_id>/<int:student_id>')
+api.add_resource(MultipleChoice,
+                 '/mult-choice/<int:id>',
+                 '/mult-choice/')
 
 
 # Other resources that don't return JSON
diff --git a/zesje/api/emails.py b/zesje/api/emails.py
index 60039da236d84a9727f4c8d39fb9e61dc156753b..408657ad7f8e2537b61f158b9ab65132a8b9d44b 100644
--- a/zesje/api/emails.py
+++ b/zesje/api/emails.py
@@ -57,27 +57,27 @@ def render_email(exam_id, student_id, template):
 
 
 def build_email(exam_id, student_id, template, attach, from_address, copy_to=None):
-        student = Student.query.get(student_id)
-        if student is None:
-            abort(
-                404,
-                message=f"Student #{student_id} does not exist"
-            )
-        if not student.email:
-            abort(
-                409,
-                message=f'Student #{student_id} has no email address'
-            )
-
-        return emails.build(
-            student.email,
-            render_email(exam_id, student_id, template),
-            emails.build_solution_attachment(exam_id, student_id)
-            if attach
-            else None,
-            copy_to=copy_to,
-            email_from=from_address,
+    student = Student.query.get(student_id)
+    if student is None:
+        abort(
+            404,
+            message=f"Student #{student_id} does not exist"
         )
+    if not student.email:
+        abort(
+            409,
+            message=f'Student #{student_id} has no email address'
+        )
+
+    return emails.build(
+        student.email,
+        render_email(exam_id, student_id, template),
+        emails.build_solution_attachment(exam_id, student_id)
+        if attach
+        else None,
+        copy_to=copy_to,
+        email_from=from_address,
+    )
 
 
 class EmailTemplate(Resource):
diff --git a/zesje/api/exams.py b/zesje/api/exams.py
index 706e7413cfadccc715fec6a7006482f0cc43d500..a1c449a05bd1ad5d5a1f50a75c7e3698d66114e5 100644
--- a/zesje/api/exams.py
+++ b/zesje/api/exams.py
@@ -25,6 +25,33 @@ def _get_exam_dir(exam_id):
     )
 
 
+def get_cb_data_for_exam(exam):
+    """
+    Returns all multiple choice question check boxes for one specific exam
+
+    Parameters
+    ----------
+        exam: the exam
+
+    Returns
+    -------
+        A list of tuples with checkbox data.
+        Each tuple is represented as (x, y, page, label)
+
+        Where
+        x: x position
+        y: y position
+        page: page number
+        label: checkbox label
+    """
+    cb_data = []
+    for problem in exam.problems:
+        page = problem.widget.page
+        cb_data += [(cb.x, cb.y, page, cb.label) for cb in problem.mc_options]
+
+    return cb_data
+
+
 class Exams(Resource):
 
     def get(self, exam_id=None):
@@ -111,30 +138,30 @@ class Exams(Resource):
             return dict(status=404, message='Exam does not exist.'), 404
 
         submissions = [
-                {
-                    'id': sub.copy_number,
-                    'student': {
-                            'id': sub.student.id,
-                            'firstName': sub.student.first_name,
-                            'lastName': sub.student.last_name,
-                            'email': sub.student.email
-                    } if sub.student else None,
-                    'validated': sub.signature_validated,
-                    'problems': [
-                        {
-                            'id': sol.problem.id,
-                            'graded_by': {
-                                'id': sol.graded_by.id,
-                                'name': sol.graded_by.name
-                            } if sol.graded_by else None,
-                            'graded_at': sol.graded_at.isoformat() if sol.graded_at else None,
-                            'feedback': [
-                                fb.id for fb in sol.feedback
-                            ],
-                            'remark': sol.remarks if sol.remarks else ""
-                        } for sol in sub.solutions  # Sorted by sol.problem_id
-                    ],
-                } for sub in exam.submissions
+            {
+                'id': sub.copy_number,
+                'student': {
+                    'id': sub.student.id,
+                    'firstName': sub.student.first_name,
+                    'lastName': sub.student.last_name,
+                    'email': sub.student.email
+                } if sub.student else None,
+                'validated': sub.signature_validated,
+                'problems': [
+                    {
+                        'id': sol.problem.id,
+                        'graded_by': {
+                            'id': sol.graded_by.id,
+                            'name': sol.graded_by.name
+                        } if sol.graded_by else None,
+                        'graded_at': sol.graded_at.isoformat() if sol.graded_at else None,
+                        'feedback': [
+                            fb.id for fb in sol.feedback
+                        ],
+                        'remark': sol.remarks if sol.remarks else ""
+                    } for sol in sub.solutions  # Sorted by sol.problem_id
+                ],
+            } for sub in exam.submissions
         ]
         # Sort submissions by selecting those with students assigned, then by
         # student number, then by copy number.
@@ -171,8 +198,22 @@ class Exams(Resource):
                         'y': prob.widget.y,
                         'width': prob.widget.width,
                         'height': prob.widget.height,
+                        'type': prob.widget.type
                     },
-                    'graded': any([sol.graded_by is not None for sol in prob.solutions])
+                    'graded': any([sol.graded_by is not None for sol in prob.solutions]),
+                    'mc_options': [
+                        {
+                            'id': mc_option.id,
+                            'label': mc_option.label,
+                            'feedback_id': mc_option.feedback_id,
+                            'widget': {
+                                'name': mc_option.name,
+                                'x': mc_option.x,
+                                'y': mc_option.y,
+                                'type': mc_option.type
+                            }
+                        } for mc_option in prob.mc_options
+                    ]
                 } for prob in exam.problems  # Sorted by prob.id
             ],
             'widgets': [
@@ -181,6 +222,7 @@ class Exams(Resource):
                     'name': widget.name,
                     'x': widget.x,
                     'y': widget.y,
+                    'type': widget.type
                 } for widget in exam.widgets  # Sorted by widget.id
             ],
             'finalized': exam.finalized,
@@ -332,13 +374,16 @@ class ExamGeneratedPdfs(Resource):
         generated_pdfs_dir = self._get_generated_exam_dir(exam_dir)
         os.makedirs(generated_pdfs_dir, exist_ok=True)
 
+        cb_data = get_cb_data_for_exam(exam)
+
         generate_pdfs(
             exam_path,
             exam.token,
             copy_nums,
             pdf_paths,
             student_id_widget.x, student_id_widget.y,
-            barcode_widget.x, barcode_widget.y
+            barcode_widget.x, barcode_widget.y,
+            cb_data
         )
 
     post_parser = reqparse.RequestParser()
@@ -488,13 +533,15 @@ class ExamPreview(Resource):
 
         exam_path = os.path.join(exam_dir, 'exam.pdf')
 
+        cb_data = get_cb_data_for_exam(exam)
         generate_pdfs(
             exam_path,
             exam.token[:5] + 'PREVIEW',
             [1519],
             [output_file],
             student_id_widget.x, student_id_widget.y,
-            barcode_widget.x, barcode_widget.y
+            barcode_widget.x, barcode_widget.y,
+            cb_data
         )
 
         output_file.seek(0)
diff --git a/zesje/api/feedback.py b/zesje/api/feedback.py
index 6475bad4f8660207a3aa60548c71f4cd02a52064..4abf904cac58751d6cb5bd3affc837a2ee3eb04f 100644
--- a/zesje/api/feedback.py
+++ b/zesje/api/feedback.py
@@ -125,6 +125,9 @@ class Feedback(Resource):
         problem = fb.problem
         if problem.id != problem_id:
             return dict(status=409, message="Feedback does not match the problem."), 409
+        if fb.mc_option and problem.exam.finalized:
+            return dict(status=401, message='Cannot delete feedback option'
+                                            + ' attached to a multiple choice option in a finalized exam.'), 401
 
         db.session.delete(fb)
 
@@ -137,4 +140,10 @@ class Feedback(Resource):
                 solution.grader_id = None
                 solution.graded_at = None
 
+        # Delete mc_options associated with this feedback option
+        if fb.mc_option:
+            db.session.delete(fb.mc_option)
+
         db.session.commit()
+
+        return dict(status=200, message=f"Feedback option with id {feedback_id} deleted."), 200
diff --git a/zesje/api/mult_choice.py b/zesje/api/mult_choice.py
new file mode 100644
index 0000000000000000000000000000000000000000..29eaa61dfa5363f18a0a7afb91d78893da1a678d
--- /dev/null
+++ b/zesje/api/mult_choice.py
@@ -0,0 +1,161 @@
+from flask_restful import Resource, reqparse
+
+from ..database import db, MultipleChoiceOption, FeedbackOption
+
+
+def set_mc_data(mc_entry, name, x, y, mc_type, feedback_id, label):
+    """Sets the data of a MultipleChoiceOption ORM object.
+
+    Parameters:
+    -----------
+    mc_entry: The MultipleChoiceOption object
+    name: The name of the MultipleChoiceOption widget
+    x: the x-position of the MultipleChoiceOption object.
+    y: the y-position of the MultipleChoiceOption object.
+    type: the polymorphic type used to distinguish the MultipleChoiceOption widget
+        from other widgets
+    feedback_id: the feedback the MultipleChoiceOption refers to
+    label: label for the checkbox that this MultipleChoiceOption represents
+    """
+    mc_entry.name = name
+    mc_entry.x = x
+    mc_entry.y = y
+    mc_entry.type = mc_type
+    mc_entry.feedback_id = feedback_id
+    mc_entry.label = label
+
+
+class MultipleChoice(Resource):
+
+    put_parser = reqparse.RequestParser()
+
+    # Arguments that have to be supplied in the request body
+    put_parser.add_argument('name', type=str, required=True)
+    put_parser.add_argument('x', type=int, required=True)
+    put_parser.add_argument('y', type=int, required=True)
+    put_parser.add_argument('label', type=str, required=False)
+    put_parser.add_argument('problem_id', type=int, required=True)  # Used for FeedbackOption
+
+    def put(self, id=None):
+        """Adds or updates a multiple choice option to the database
+
+        If the parameter id is not present, a new multiple choice question
+        will be inserted with the data provided in the request body.
+
+        For each new multiple choice option, a feedback option that links to
+        the multiple choice option is inserted into the database. The new
+        feedback option also refers to same problem as the MultipleChoiceOption
+
+        Parameters
+        ----------
+            id: The id of the multiple choice option
+        """
+
+        args = self.put_parser.parse_args()
+
+        # Get request arguments
+        name = args['name']
+        x = args['x']
+        y = args['y']
+        label = args['label']
+        problem_id = args['problem_id']
+
+        # TODO: Set type here or add to request?
+        mc_type = 'mcq_widget'
+
+        if not id:
+            # Insert new empty feedback option that links to the same problem, with the label as name
+            new_feedback_option = FeedbackOption(problem_id=problem_id, text=label)
+            db.session.add(new_feedback_option)
+            db.session.commit()
+
+            # Insert new entry into the database
+            mc_entry = MultipleChoiceOption()
+            set_mc_data(mc_entry, name, x, y, mc_type, new_feedback_option.id, label)
+
+            db.session.add(mc_entry)
+            db.session.commit()
+
+            return dict(status=200, mult_choice_id=mc_entry.id, feedback_id=new_feedback_option.id,
+                        message=f'New multiple choice question with id {mc_entry.id} inserted. '
+                        + f'New feedback option with id {new_feedback_option.id} inserted.'), 200
+
+        # Update existing entry otherwise
+        mc_entry = MultipleChoiceOption.query.get(id)
+
+        if not mc_entry:
+            return dict(status=404, message=f"Multiple choice question with id {id} does not exist"), 404
+
+        set_mc_data(mc_entry, name, x, y, mc_type, label)
+        db.session.commit()
+
+        return dict(status=200, message=f'Multiple choice question with id {id} updated'), 200
+
+    def get(self, id):
+        """Fetches multiple choice option from the database
+
+        Parameters
+        ----------
+            id: The ID of the multiple choice option in the database
+
+        Returns
+        -------
+            A JSON object with the multiple choice option data
+        """
+        mult_choice = MultipleChoiceOption.query.get(id)
+
+        if not mult_choice:
+            return dict(status=404, message=f'Multiple choice question with id {id} does not exist.'), 404
+
+        json = {
+            'id': mult_choice.id,
+            'name': mult_choice.name,
+            'x': mult_choice.x,
+            'y': mult_choice.y,
+            'type': mult_choice.type,
+            'feedback_id': mult_choice.feedback_id
+        }
+
+        # Nullable database fields
+        if mult_choice.label:
+            json['label'] = mult_choice.label
+
+        return json
+
+    def delete(self, id):
+        """Deletes a multiple choice option from the database.
+        Also deletes the associated feedback option with this multiple choice option.
+
+        An error will be thrown if the user tries to delete a feedback option
+        associated with a multiple choice option in a finalized exam.
+
+        Parameters
+        ----------
+            id: The ID of the multiple choice option in the database
+
+        Returns
+        -------
+            A message indicating success or failure
+        """
+        mult_choice = MultipleChoiceOption.query.get(id)
+
+        if not mult_choice:
+            return dict(status=404, message=f'Multiple choice question with id {id} does not exist.'), 404
+
+        if not mult_choice.feedback:
+            return dict(status=404, message=f'Multiple choice question with id {id}'
+                        + ' is not associated with a feedback option.'), 404
+
+        # Check if the exam is finalized, do not delete the multiple choice option otherwise
+        exam = mult_choice.feedback.problem.exam
+
+        if exam.finalized:
+            return dict(status=401, message='Cannot delete feedback option'
+                        + ' attached to a multiple choice option in a finalized exam.'), 401
+
+        db.session.delete(mult_choice)
+        db.session.delete(mult_choice.feedback)
+        db.session.commit()
+
+        return dict(status=200, message=f'Multiple choice question with id {id} deleted.'
+                    + f'Feedback option with id {mult_choice.feedback_id} deleted.'), 200
diff --git a/zesje/api/problems.py b/zesje/api/problems.py
index 41c24f2ed1cd5f7909611e86fe2156efe719cf7a..28b005d3f5f9054d6f083dc82f9852c91c66c230 100644
--- a/zesje/api/problems.py
+++ b/zesje/api/problems.py
@@ -108,6 +108,9 @@ class Problems(Resource):
             # Delete all solutions associated with this problem
             for sol in problem.solutions:
                 db.session.delete(sol)
+            # Delete all multiple choice options associated with this problem
+            for mc_option in problem.mc_options:
+                db.session.delete(mc_option)
             db.session.delete(problem.widget)
             db.session.delete(problem)
             db.session.commit()
diff --git a/zesje/database.py b/zesje/database.py
index 3b6df0459314ad8212776ba1655ea8e1385f3142..5f4bc29a061dab409da4bcb6054f62958c95f06d 100644
--- a/zesje/database.py
+++ b/zesje/database.py
@@ -8,6 +8,7 @@ from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, Foreign
 from flask_sqlalchemy.model import BindMetaMixin, Model
 from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
 from sqlalchemy.orm.session import object_session
+from sqlalchemy.ext.hybrid import hybrid_property
 
 
 # Class for NOT automatically determining table names
@@ -100,6 +101,10 @@ class Problem(db.Model):
     solutions = db.relationship('Solution', backref='problem', lazy=True)
     widget = db.relationship('ProblemWidget', backref='problem', uselist=False, lazy=True)
 
+    @hybrid_property
+    def mc_options(self):
+        return [feedback_option.mc_option for feedback_option in self.feedback_options if feedback_option.mc_option]
+
 
 class FeedbackOption(db.Model):
     """feedback option"""
@@ -109,6 +114,7 @@ class FeedbackOption(db.Model):
     text = Column(Text, nullable=False)
     description = Column(Text, nullable=True)
     score = Column(Integer, nullable=True)
+    mc_option = db.relationship('MultipleChoiceOption', backref='feedback', cascade='delete', uselist=False, lazy=True)
 
 
 # Table for many to many relationship of FeedbackOption and Solution
@@ -160,6 +166,18 @@ class Widget(db.Model):
     }
 
 
+class MultipleChoiceOption(Widget):
+    __tablename__ = 'mc_option'
+    id = Column(Integer, ForeignKey('widget.id'), primary_key=True, autoincrement=True)
+
+    label = Column(String, nullable=True)
+    feedback_id = Column(Integer, ForeignKey('feedback_option.id'), nullable=False)
+
+    __mapper_args__ = {
+        'polymorphic_identity': 'mcq_widget'
+    }
+
+
 class ExamWidget(Widget):
     __tablename__ = 'exam_widget'
     id = Column(Integer, ForeignKey('widget.id'), primary_key=True, nullable=False)
diff --git a/zesje/emails.py b/zesje/emails.py
index 212952086b4a4c002537e123942c4033ee74eb21..8e3571608e501a0476cfcb357104d3cf2674184c 100644
--- a/zesje/emails.py
+++ b/zesje/emails.py
@@ -103,7 +103,7 @@ def send(
     server_type = smtplib.SMTP_SSL if use_ssl else smtplib.SMTP
     with server_type(server, port) as s:
         if user and password:
-                s.login(user, password)
+            s.login(user, password)
         for identifier, message in messages.items():
             recipients = [
                 *message['To'].split(','),
diff --git a/zesje/images.py b/zesje/images.py
index fd867abca0c07cbbcd26b8663cec2ea7e96b6780..ff8669e5b04aa77aa882f0bdb3acbaf9901ab450 100644
--- a/zesje/images.py
+++ b/zesje/images.py
@@ -2,6 +2,8 @@
 
 import numpy as np
 
+from operator import sub, add
+
 
 def guess_dpi(image_array):
     h, *_ = image_array.shape
@@ -34,3 +36,97 @@ def get_box(image_array, box, padding=0.3):
     top, bottom = max(0, min(box[0], h)), max(1, min(box[1], h))
     left, right = max(0, min(box[2], w)), max(1, min(box[3], w))
     return image_array[top:bottom, left:right]
+
+
+def fix_corner_markers(corner_keypoints, shape):
+    """
+    Corrects the list of corner markers if only three corner markers are found.
+    This function raises if less than three corner markers are detected.
+
+    Parameters
+    ----------
+    corner_keypoints :
+        List of corner marker locations as tuples
+    shape :
+        Shape of the image in (x, y, dim)
+
+    Returns
+    -------
+    corner_keypoints :
+        A list of four corner markers.
+    """
+
+    if len(corner_keypoints) == 4:
+        return corner_keypoints
+
+    if len(corner_keypoints) < 3:
+        raise RuntimeError("Fewer then 3 corner markers found")
+
+    x_sep = shape[1] / 2
+    y_sep = shape[0] / 2
+
+    top_left = [(x, y) for x, y in corner_keypoints if x < x_sep and y < y_sep]
+    bottom_left = [(x, y) for x, y in corner_keypoints if x < x_sep and y > y_sep]
+    top_right = [(x, y) for x, y in corner_keypoints if x > x_sep and y < y_sep]
+    bottom_right = [(x, y) for x, y in corner_keypoints if x > x_sep and y > y_sep]
+
+    missing_point = ()
+
+    if not top_left:
+        # Top left point is missing
+        (dx, dy) = tuple(map(sub, top_right[0], bottom_right[0]))
+        missing_point = tuple(map(add, bottom_left[0], (dx, dy)))
+
+    elif not bottom_left:
+        # Bottom left point is missing
+        (dx, dy) = tuple(map(sub, top_right[0], bottom_right[0]))
+        missing_point = tuple(map(sub, top_left[0], (dx, dy)))
+
+    elif not top_right:
+        # Top right point is missing
+        (dx, dy) = tuple(map(sub, top_left[0], bottom_left[0]))
+        missing_point = tuple(map(add, bottom_right[0], (dx, dy)))
+
+    elif not bottom_right:
+        # bottom right
+        (dx, dy) = tuple(map(sub, top_left[0], bottom_left[0]))
+        missing_point = tuple(map(sub, top_right[0], (dx, dy)))
+
+    corner_keypoints.append(missing_point)
+    return corner_keypoints
+
+
+def box_is_filled(image_array, box_coords, padding=0.3, threshold=150, pixels=False):
+    """
+    Determines if a box is filled
+
+    Parameters:
+    -----------
+    image_array : 2D or 3D array
+        The image source.
+    box_coords : 4 floats (top, bottom, left, right)
+        Coordinates of the bounding box in inches or pixels. By due to differing
+        traditions, box coordinates are counted from the bottom left of the
+        image, while image array coordinates are from the top left.
+    padding : float
+        Padding around box borders in inches.
+    threshold : int
+        Optional threshold value to determine minimal 'darkness'
+        to consider a box to be filled in
+    pixels : boolean
+        Whether the box coordinates are entered as pixels instead of inches.
+    """
+
+    # Divide by DPI if pixel coordinates are used
+    if pixels:
+        box_coords /= guess_dpi(image_array)
+
+    box_img = get_box(image_array, box_coords, padding)
+
+    # Check if the coordinates are outside of the image
+    if box_img.size == 0:
+        raise RuntimeError("Box coordinates are outside of image")
+
+    avg = np.average(box_img)
+
+    return avg < threshold
diff --git a/zesje/pdf_generation.py b/zesje/pdf_generation.py
index f0bc3ebb00843818081e61bf45913cdf8127ff31..09bf9641bd13e631eb02a163f69ef2959b7955d6 100644
--- a/zesje/pdf_generation.py
+++ b/zesje/pdf_generation.py
@@ -12,7 +12,7 @@ output_pdf_filename_format = '{0:05d}.pdf'
 
 
 def generate_pdfs(exam_pdf_file, exam_id, copy_nums, output_paths, id_grid_x,
-                  id_grid_y, datamatrix_x, datamatrix_y):
+                  id_grid_y, datamatrix_x, datamatrix_y, cb_data=None):
     """
     Generate the final PDFs from the original exam PDF.
 
@@ -24,7 +24,6 @@ def generate_pdfs(exam_pdf_file, exam_id, copy_nums, output_paths, id_grid_x,
     If maximum interchangeability with version 1 QR codes is desired (error
     correction level M), use exam IDs composed of only uppercase letters, and
     composed of at most 12 letters.
-
     Parameters
     ----------
     exam_pdf_file : file object or str
@@ -43,6 +42,9 @@ def generate_pdfs(exam_pdf_file, exam_id, copy_nums, output_paths, id_grid_x,
         The x coordinate where the DataMatrix code should be placed
     datamatrix_y : int
         The y coordinate where the DataMatrix code should be placed
+    cb_data : list[ (int, int, int, str)]
+        The data needed for drawing a checkbox, namely: the x coordinate; y coordinate; page number and label
+
     """
     exam_pdf = PdfReader(exam_pdf_file)
     mediabox = exam_pdf.pages[0].MediaBox
@@ -56,7 +58,7 @@ def generate_pdfs(exam_pdf_file, exam_id, copy_nums, output_paths, id_grid_x,
             overlay_canv = canvas.Canvas(overlay_file.name, pagesize=pagesize)
             _generate_overlay(overlay_canv, pagesize, exam_id, copy_num,
                               len(exam_pdf.pages), id_grid_x, id_grid_y,
-                              datamatrix_x, datamatrix_y)
+                              datamatrix_x, datamatrix_y, cb_data)
             overlay_canv.save()
 
             # Merge overlay and exam
@@ -151,6 +153,38 @@ def generate_id_grid(canv, x, y):
               textboxwidth, textboxheight)
 
 
+def generate_checkbox(canvas, x, y, label):
+    """
+    draw a checkbox and draw a  singel character label ontop of the checkbox
+
+    Parameters
+    ----------
+    canvas : reportlab canvas object
+
+    x : int
+        the x coordinate of the top left corner of the box in points (pt)
+    y : int
+        the y coordinate of the top left corner of the box in points (pt)
+    label: str
+        A string representing the label that is drawn on top of the box, will only take the first character
+
+    """
+    fontsize = 11  # Size of font
+    margin = 5  # Margin between elements and sides
+    markboxsize = fontsize - 2  # Size of checkboxes boxes
+    x_label = x + 1  # location of the label
+    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
+    if (label and not (len(label) == 0)):
+        canvas.setFont('Helvetica', fontsize)
+        canvas.drawString(x_label, y_label, label[0])
+
+    canvas.rect(x, box_y, markboxsize, markboxsize)
+
+
 def generate_datamatrix(exam_id, page_num, copy_num):
     """
     Generates a DataMatrix code to be used on a page.
@@ -187,7 +221,7 @@ def generate_datamatrix(exam_id, page_num, copy_num):
 
 
 def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x,
-                      id_grid_y, datamatrix_x, datamatrix_y):
+                      id_grid_y, datamatrix_x, datamatrix_y, cb_data=None):
     """
     Generates an overlay ('watermark') PDF, which can then be overlaid onto
     the exam PDF.
@@ -221,6 +255,9 @@ def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x,
         The x coordinate where the DataMatrix codes should be placed
     datamatrix_y : int
         The y coordinate where the DataMatrix codes should be placed
+    cb_data : list[ (int, int, int, str)]
+        The data needed for drawing a checkbox, namely: the x coordinate; y coordinate; page number and label
+
     """
 
     # Font settings for the copy number (printed under the datamatrix)
@@ -233,6 +270,17 @@ def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x,
     # ID grid on first page only
     generate_id_grid(canv, id_grid_x, id_grid_y)
 
+    # create index for list of checkbox data and sort the data on page
+    if cb_data:
+        index = 0
+        max_index = len(cb_data)
+        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:
+        index = 0
+        max_index = 0
+
     for page_num in range(num_pages):
         _add_corner_markers_and_bottom_bar(canv, pagesize)
 
@@ -246,6 +294,13 @@ def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x,
             datamatrix_x, datamatrix_y_adjusted - fontsize,
             f" # {copy_num}"
         )
+
+        # call generate for all checkboxes that belong to the current page
+        while index < max_index and cb_data[index][2] <= page_num:
+            x, y, _, label = cb_data[index]
+            generate_checkbox(canv, x, y, label)
+            index += 1
+
         canv.showPage()
 
 
diff --git a/zesje/pregrader.py b/zesje/pregrader.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b02521d437a3594579b3be2cfa91b8188d194d2
--- /dev/null
+++ b/zesje/pregrader.py
@@ -0,0 +1,139 @@
+import cv2
+import numpy as np
+
+from .database import db, Solution
+from .images import guess_dpi, get_box, fix_corner_markers
+
+
+def add_feedback_to_solution(sub, exam, page, page_img, corner_keypoints):
+    """
+    Adds the multiple choice options that are identified as marked as a feedback option to a solution
+
+    Parameters
+    ------
+    sub : Submission
+        the current submission
+    exam : Exam
+        the current exam
+    page_img : Image
+        image of the page
+    corner_keypoints : array
+        locations of the corner keypoints as (x, y) tuples
+    """
+    problems_on_page = [problem for problem in exam.problems if problem.widget.page == page]
+
+    fixed_corner_keypoints = fix_corner_markers(corner_keypoints, page_img.shape)
+
+    x_min = min(point[0] for point in fixed_corner_keypoints)
+    y_min = min(point[1] for point in fixed_corner_keypoints)
+    top_left_point = (x_min, y_min)
+
+    for problem in problems_on_page:
+        sol = Solution.query.filter(Solution.problem_id == problem.id, Solution.submission_id == sub.id).one_or_none()
+
+        for mc_option in problem.mc_options:
+            box = (mc_option.x, mc_option.y)
+
+            if box_is_filled(box, page_img, top_left_point):
+                feedback = mc_option.feedback
+                sol.feedback.append(feedback)
+                db.session.commit()
+
+
+def box_is_filled(box, page_img, corner_keypoints, marker_margin=72/2.54, threshold=225, cut_padding=0.1, box_size=11):
+    """
+    A function that finds the checkbox in a general area and then checks if it is filled in.
+
+    Params
+    ------
+    box: (int, int)
+        The coordinates of the top left (x,y) of the checkbox in points.
+    page_img: np.array
+        A numpy array of the image scan
+    corner_keypoints: (float,float)
+        The x coordinate of the left markers and the y coordinate of the top markers,
+        used as point of reference since scans can deviate from the original.
+        (x,y) are both in pixels.
+    marker_margin: float
+        The margin between the corner markers and the edge of a page when generated.
+    threshold: int
+        the threshold needed for a checkbox to be considered marked range is between 0 (fully black)
+        and 255 (absolutely white).
+    cut_padding: float
+        The extra padding when retrieving an area where the checkbox is in inches.
+    box_size: int
+        the size of the checkbox in points.
+
+    Output
+    ------
+    True if the box is marked, else False.
+    """
+
+    # shouldn't be needed, but some images are drawn a bit weirdly
+    y_shift = 5
+    # create an array with y top, y bottom, x left and x right. use the marker margin to allign to the page.
+    coords = np.asarray([box[1] - marker_margin + y_shift, box[1] + box_size - marker_margin + y_shift,
+                        box[0] - marker_margin, box[0] + box_size - marker_margin])/72
+
+    # add the actually margin from the scan to corner markers to the coords in inches
+    dpi = guess_dpi(page_img)
+    coords[0] = coords[0] + corner_keypoints[1]/dpi
+    coords[1] = coords[1] + corner_keypoints[1]/dpi
+    coords[2] = coords[2] + corner_keypoints[0]/dpi
+    coords[3] = coords[3] + corner_keypoints[0]/dpi
+
+    # get the box where we think the box is
+    cut_im = get_box(page_img, coords, padding=cut_padding)
+
+    # convert to grayscale
+    gray_im = cv2.cvtColor(cut_im, cv2.COLOR_BGR2GRAY)
+    # apply threshold to only have black or white
+    _, bin_im = cv2.threshold(gray_im, 150, 255, cv2.THRESH_BINARY)
+
+    h_bin, w_bin, *_ = bin_im.shape
+    # create a mask that gets applied when floodfill the white
+    mask = np.zeros((h_bin+2, w_bin+2), np.uint8)
+    flood_im = bin_im.copy()
+    # fill the image from the top left
+    cv2.floodFill(flood_im, mask, (0, 0),  0)
+    # fill it from the bottom right just in case the top left doesn't cover all the white
+    cv2.floodFill(flood_im, mask, (h_bin-2, w_bin-2), 0)
+
+    # find white parts
+    coords = cv2.findNonZero(flood_im)
+    # Find a bounding box of the white parts
+    x, y, w, h = cv2.boundingRect(coords)
+    # cut the image to this box
+    res_rect = bin_im[y:y+h, x:x+w]
+
+    # the size in pixels we expect the drawn box to
+    box_size_px = box_size*dpi / 72
+
+    # if the rectangle is bigger (higher) than expected, cut the image up a bit
+    if h > 1.5 * box_size_px:
+        print("in h resize")
+        y_partition = 0.333
+        # try getting another bounding box on bottom 2/3 of the screen
+        coords2 = cv2.findNonZero(flood_im[y + int(y_partition * h): y + h, x: x+w])
+        x2, y2, w2, h2 = cv2.boundingRect(coords2)
+        # add these coords to create a new bounding box we are looking at
+        new_y = y+y2 + int(y_partition * h)
+        new_x = x + x2
+        res_rect = bin_im[new_y:new_y + h2, new_x:new_x + w2]
+
+    else:
+        new_x, new_y, w2, h2 = x, y, w, h
+
+    # do the same for width
+    if w2 > 1.5 * box_size_px:
+        # usually the checkbox is somewhere in the bottom left of the bounding box
+        coords3 = cv2.findNonZero(flood_im[new_y: new_y + h2, new_x: new_x + int(0.66 * w2)])
+        x3, y3, w3, h3 = cv2.boundingRect(coords3)
+        res_rect = bin_im[new_y + y3: new_y + y3 + h3, new_x + x3: new_x + x3 + w3]
+
+    # if the found box is smaller than a certain threshold
+    # it means that we only found a little bit of white and the box is filled
+    res_x, res_y, *_ = res_rect.shape
+    if res_x < 0.333 * box_size_px or res_y < 0.333 * box_size_px:
+        return True
+    return np.average(res_rect) < threshold
diff --git a/zesje/scans.py b/zesje/scans.py
index bcbde9f68e30f93eea06f2403cc5d0a35844f6ce..8d99d0f4186cb4b5f84a3627dfcd584d4af3531c 100644
--- a/zesje/scans.py
+++ b/zesje/scans.py
@@ -17,7 +17,7 @@ from .database import db, Scan, Exam, Page, Student, Submission, Solution, ExamW
 from .datamatrix import decode_raw_datamatrix
 from .images import guess_dpi, get_box
 from .factory import make_celery
-
+from .pregrader import add_feedback_to_solution
 
 ExtractedBarcode = namedtuple('ExtractedBarcode', ['token', 'copy', 'page'])
 
@@ -54,7 +54,7 @@ def process_pdf(scan_id):
         # TODO: When #182 is implemented, properly separate user-facing
         #       messages (written to DB) from developer-facing messages,
         #       which should be written into the log.
-        write_pdf_status(scan_id, 'error', "Unexpected error: " + str(error))
+        write_pdf_status(scan_id, 'error', f"Unexpected error: {error}")
 
 
 def _process_pdf(scan_id, app_config):
@@ -91,8 +91,8 @@ def _process_pdf(scan_id, app_config):
                     print(description)
                     failures.append(page)
             except Exception as e:
-                report_error(f'Error processing page {page}: {e}')
-                return
+                report_error(f'Error processing page {e}')
+                raise
     except Exception as e:
         report_error(f"Failed to read pdf: {e}")
         raise
@@ -337,7 +337,13 @@ def process_page(image_data, exam_config, output_dir=None, strict=False):
     else:
         return True, "Testing, image not saved and database not updated."
 
-    update_database(image_path, barcode)
+    sub, exam = update_database(image_path, barcode)
+
+    try:
+        add_feedback_to_solution(sub, exam, barcode.page, image_array, corner_keypoints)
+    except RuntimeError as e:
+        if strict:
+            return False, str(e)
 
     if barcode.page == 0:
         description = guess_student(
@@ -385,8 +391,12 @@ def update_database(image_path, barcode):
 
     Returns
     -------
-    signature_validated : bool
-        If the corresponding submission has a validated signature.
+    sub, exam where
+
+    sub : Submission
+        the current submission
+    exam : Exam
+        the current exam
     """
     exam = Exam.query.filter(Exam.token == barcode.token).first()
     if exam is None:
@@ -406,6 +416,8 @@ def update_database(image_path, barcode):
 
     db.session.commit()
 
+    return sub, exam
+
 
 def decode_barcode(image, exam_config):
     """Extract a barcode from a PIL Image."""