diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a44111ecf75ba7e489250f88e1343f8794983603..cc4d80dde01a0addf65b1df34876b52c93cf913f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,6 +5,8 @@ stages:
 
 job_lint_flake8:
   stage: lint
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
     - flake8 --version
@@ -12,6 +14,8 @@ job_lint_flake8:
     
 job_lint_pylint:
   stage: lint
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
     - pylint --version
@@ -19,6 +23,8 @@ job_lint_pylint:
 
 job_lint_bandit:
   stage: lint
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
     - bandit --version
@@ -26,6 +32,8 @@ job_lint_bandit:
 
 job_lint_pydocstyle:
   stage: lint
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
     - pydocstyle --version
@@ -33,6 +41,8 @@ job_lint_pydocstyle:
       
 job_test_gui_ubuntu_vnc:
   stage: test
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
     # -platform because running with a screen is not supported
@@ -57,6 +67,8 @@ job_test_gui_ubuntu:
 
 job_pages_smoke_test:
   stage: deploy
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
   - mkdir public
@@ -70,6 +82,8 @@ job_pages_smoke_test:
 # name has to be pages
 pages:
   stage: deploy
+  tags:
+    - ci-ubuntu-executor-docker
   image: morkolai/soitool-ci
   script:
   - mkdir public
diff --git a/requirements.txt b/requirements.txt
index a3389fd958e487a8943bc582d8ebdc57329a4bf3..45f849a7607dc162721fd3fa1218599d16f66819 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/scripts/.pylintrc b/scripts/.pylintrc
index 01e79b9dda28b3eefb8acdaa22c29477f31ae2b2..6262e5ddd5bbbd19ecdd7bd226c74675c1477091 100644
--- a/scripts/.pylintrc
+++ b/scripts/.pylintrc
@@ -286,7 +286,7 @@ indent-after-paren=4
 indent-string='    '
 
 # Maximum number of characters on a single line.
-max-line-length=100
+max-line-length=79
 
 # Maximum number of lines in a module.
 max-module-lines=1000
@@ -404,6 +404,8 @@ function-naming-style=snake_case
 
 # Good variable names which should always be accepted, separated by a comma.
 good-names=app,
+           x,
+           y,
            ok,
            i,
            j,
@@ -503,7 +505,7 @@ valid-metaclass-classmethod-first-arg=cls
 max-args=5
 
 # Maximum number of attributes for a class (see R0902).
-max-attributes=7
+max-attributes=15
 
 # Maximum number of boolean expressions in an if statement.
 max-bool-expr=5
diff --git a/soitool/main_window.py b/soitool/main_window.py
index 042f25a93784dabad3bd5a6a6bb8dd72f517211c..1adeebd03878548824c404683c4d025bd41f8c33 100644
--- a/soitool/main_window.py
+++ b/soitool/main_window.py
@@ -6,11 +6,13 @@ out to separate modules.
 """
 import sys
 import os
-from PySide2.QtCore import Qt
+from PySide2.QtCore import QRectF, QPoint, QTimer, Qt
 from PySide2.QtWidgets import QTabWidget, QWidget, QMainWindow, \
     QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \
-    QAbstractItemView, QListWidget, QListWidgetItem, QAction
-from PySide2.QtGui import QIcon
+    QAbstractItemView, QListWidget, QListWidgetItem, QAction, QGraphicsScene, \
+    QGraphicsView, QScrollArea, QGraphicsRectItem
+from PySide2.QtGui import QBrush, QIcon, QPalette, QFont, QPixmap
+from soitool.modules.module_table import TableModule
 
 
 class MainWindow(QMainWindow):
@@ -110,6 +112,8 @@ class SOIWorkspaceWidget(QWidget):
     def __init__(self):
         super().__init__()
 
+        self.soi = SOI()
+
         self.layout_wrapper = QHBoxLayout()
         self.layout_sidebar = QVBoxLayout()
 
@@ -119,7 +123,9 @@ class SOIWorkspaceWidget(QWidget):
         self.button_new_module.setStatusTip("Legg til en ny modul")
         self.button_setup = QPushButton("Oppsett")
         self.list_modules = QListWidget()
-        self.view = ViewArea()
+        self.view = InlineEditableSOIView(self.soi)
+        self.widget_sidebar = QWidget()
+        self.widget_sidebar.setFixedWidth(200)
 
         # prepare module list
         self.setup_list_modules()
@@ -132,10 +138,10 @@ class SOIWorkspaceWidget(QWidget):
         self.layout_sidebar.addWidget(QListWidget(), 1)
         self.layout_sidebar.addWidget(self.button_new_module)
         self.layout_sidebar.addWidget(self.button_setup)
-        self.layout_wrapper.addLayout(self.layout_sidebar)
+        self.widget_sidebar.setLayout(self.layout_sidebar)
+        self.layout_wrapper.addWidget(self.widget_sidebar)
         self.layout_wrapper.addWidget(self.view)
 
-        self.setFixedWidth(200)
         self.setLayout(self.layout_wrapper)
 
     def setup_list_modules(self):
@@ -168,19 +174,315 @@ class SOIWorkspaceWidget(QWidget):
             self.list_modules.insertItem(i, item)
 
 
-class ViewArea(QWidget):
-    """Widget som kan byttes ut med view, edit etc."""
+class SOI():
+    """Temporary representation of SOI.
+
+    Holds all info about an SOI necessary to view and edit it.
+    """
+
+    A4_RATIO = 1.414
+
+    # height must be adjusted to something that will look right when real
+    # wigets are placed inside the pages
+    HEIGHT = 1700
+    WIDTH = HEIGHT * A4_RATIO
+    PADDING = 100
+    CONTENT_WIDTH = WIDTH - PADDING * 2
+    CONTENT_HEIGHT = HEIGHT - PADDING * 2
+    HEADER_HEIGHT = 100
+    MODULE_PADDING = 10
 
     def __init__(self):
+
+        # NOTE
+        # * test modules, just to have something show up on screen
+        # * not valid until reorganize has been run, as the positions must be
+        #   updated
+        self.modules = [
+            {
+                "widget": TableModule(),
+                "meta": {
+                    "x": 0,
+                    "y": 0,
+                    "page": 1
+                }
+            },
+            {
+                "widget": TableModule(),
+                "meta": {
+                    "x": 0,
+                    "y": 0,
+                    "page": 1
+                }
+            },
+            {
+                "widget": TableModule(),
+                "meta": {
+                    "x": 0,
+                    "y": 0,
+                    "page": 2
+                }
+            }
+        ]
+
+        self.reorganize()
+
+    def update_module_widget_position(self, module):
+        """Update position of module widget based on meta position.
+
+        This function is very much WIP..
+
+        Parameters
+        ----------
+        module : see description
+            should be dict of fields "meta" and "widget", where "meta" is
+            itself a dict with fields "x", "y" and "page", and "widget" is a
+            widget based on "ModuleBase"
+        """
+        distance_to_start_of_next_soi_content_y = self.CONTENT_HEIGHT + \
+            self.PADDING * 2 + self.HEADER_HEIGHT
+
+        scene_skip_distance_page_height = \
+            distance_to_start_of_next_soi_content_y * \
+            (module["meta"]["page"] - 1)
+
+        new_x = module["meta"]["x"] + self.PADDING
+        new_y = module["meta"]["y"] + self.PADDING + self.HEADER_HEIGHT + \
+            scene_skip_distance_page_height
+
+        module["widget"].set_pos(QPoint(new_x, new_y))
+
+    def reorganize(self):
+        """Update x,y positions of modules.
+
+        In the future this is where rectpack will do it's magic, for now it is
+        a WIP just to get some kind of positioning working. The positioning
+        scheme for now is as follows: for each page 1 and 2, position widgets
+        next to each other from left to right
+        """
+        x = self.MODULE_PADDING
+        first_page_modules = [module for module in self.modules
+                              if module["meta"]["page"] == 1]
+
+        for module in first_page_modules:
+            module["meta"]["x"] = x
+            module["meta"]["y"] = self.MODULE_PADDING
+
+            self.update_module_widget_position(module)
+
+            widget_width, _ = module["widget"].get_size()
+            x = x + self.MODULE_PADDING + widget_width
+
+        # NOTE the following is simply duplicated.. left like this to KISS
+        # will be replaced by rectpack anyways
+        x = self.MODULE_PADDING
+        second_page_modules = [module for module in self.modules
+                               if module["meta"]["page"] == 2]
+        for module in second_page_modules:
+            module["meta"]["x"] = x
+            module["meta"]["y"] = self.MODULE_PADDING
+
+            self.update_module_widget_position(module)
+
+            widget_width, _ = module["widget"].get_size()
+            x = x + self.MODULE_PADDING + widget_width
+
+
+class InlineEditableSOIView(QScrollArea):
+    """Widget som kan byttes ut med view, edit etc."""
+
+    def is_widget_in_scene(self, widget):
+        """Indicate wether given widget already has a proxy in the scene."""
+        for page in self.pages:
+            for proxy in page:
+                if proxy.widget() == widget:
+                    return True
+        return False
+
+    def setup_proxies(self):
+        """Set up proxies for the widgets of SOI modules."""
+        for module in self.soi.modules:
+            if not self.is_widget_in_scene(module["widget"]):
+                proxy = self.scene.addWidget(module["widget"])
+
+                while len(self.pages) < module["meta"]["page"]:
+                    self.pages.append(set())
+
+                self.pages[module["meta"]["page"] - 1].add(proxy)
+
+    def mousePressEvent(self, _):
+        """Reorganize modules when pressed.
+
+        This is a temporary way to activate reorganization of widgets. Note
+        that will not be triggered by clicks on modules in the scene
+        """
+        self.soi.reorganize()
+
+    def __init__(self, soi):
         super().__init__()
-        test = QLabel("Test")
-        layout = QHBoxLayout()
-        layout.addWidget(test)
-        self.setLayout(layout)
+
+        self.soi = soi
+
+        self.pages = []
+
+        # necessary to make the scroll area fill the space it's given
+        self.setWidgetResizable(True)
+
+        # scene and view widgets
+        self.scene = QGraphicsScene()
+        self.setup_scene()
+        self.view = QGraphicsView(self.scene)
+
+        self.setWidget(self.view)
+
+        self.setup_proxies()
+
+        self.update_drawn_pages()
+
+        # self.launch_auto_zoom()
+
+    def update_drawn_pages(self):
+        """Update drawn pages to reflect self.pages."""
+        for i, modules in enumerate(self.pages, start=1):
+
+            x = 0
+            y = self.soi.HEIGHT * max(i - 1, 0) + \
+                self.soi.PADDING * max(i - 1, 0)
+
+            # adjust page size
+            full_scene_height = y + self.soi.HEIGHT
+            self.scene.setSceneRect(QRectF(0, 0, self.soi.WIDTH,
+                                           full_scene_height))
+
+            self.draw_page(x, y)
+            self.draw_header(x + self.soi.PADDING, y + self.soi.PADDING, i)
+
+            for module in modules:
+                # redraw of pages requires modules to be moved to front again
+                module.setZValue(1)
+
+    def draw_header(self, x, y, page_number):
+        """Draw header staring at given position.
+
+        Parameters
+        ----------
+        x : int
+        y : int
+        """
+        # title
+        label_title = QLabel("SOI TITLE HERE")
+        label_title.move(x, y)
+        label_title.setStyleSheet("background-color: rgba(0,0,0,0%)")
+        label_title.setFont(QFont("Times New Roman", 50))
+        self.scene.addWidget(label_title)
+
+        # page numbering
+        page_number = QLabel("{} av {}".format(page_number, len(self.pages)))
+        page_number.setStyleSheet("background-color: rgba(0,0,0,0%)")
+        page_number.setFont(QFont("Times New Roman", 50))
+        # source: https://stackoverflow.com/a/8638114/3545896
+        # CAUTION: does not work if font is set through stylesheet
+        label_width = \
+            page_number.fontMetrics().boundingRect(page_number.text()).width()
+        page_number.move(x + (self.soi.CONTENT_WIDTH - label_width) / 2, y)
+        self.scene.addWidget(page_number)
+
+        # grading
+        grading = QLabel("UGRADERT")
+        grading.setStyleSheet("background-color: rgba(0,0,0,0%); color: red")
+        grading.setFont(QFont("Times New Roman", 50))
+        # source: https://stackoverflow.com/a/8638114/3545896
+        # CAUTION: does not work if font is set through stylesheet
+        label_width = \
+            grading.fontMetrics().boundingRect(grading.text()).width()
+        x_pos = x + self.soi.CONTENT_WIDTH - label_width - \
+            self.soi.HEADER_HEIGHT
+        grading.move(x_pos, y)
+        self.scene.addWidget(grading)
+
+        # patch
+        pixmap = QPixmap("soitool/media/HVlogo.png")
+        patch = QLabel()
+        patch.setPixmap(pixmap.scaled(self.soi.HEADER_HEIGHT,
+                                      self.soi.HEADER_HEIGHT))
+        patch.move(x + self.soi.CONTENT_WIDTH - self.soi.HEADER_HEIGHT, y)
+        self.scene.addWidget(patch)
+
+    def draw_page(self, x, y):
+        """Draw page starting at given position.
+
+        Parameters
+        ----------
+        x : int
+        y : int
+        """
+        # color the page white
+        page_background = QGraphicsRectItem(x, y, self.soi.WIDTH,
+                                            self.soi.HEIGHT)
+        page_background.setBrush(QBrush(Qt.white))
+        self.scene.addItem(page_background)
+
+        # draw borders
+        self.scene.addRect(x, y, self.soi.WIDTH, self.soi.HEIGHT)
+        self.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING,
+                           self.soi.CONTENT_WIDTH, self.soi.CONTENT_HEIGHT)
+        self.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING,
+                           self.soi.CONTENT_WIDTH, self.soi.CONTENT_HEIGHT)
+        self.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING,
+                           self.soi.CONTENT_WIDTH, self.soi.HEADER_HEIGHT)
+
+    def setup_scene(self):
+        """Prepare scene for use.
+
+        Draws borders, background, etc.
+        """
+        # sets the background color of the scene to be the appropriate
+        # system-specific background color
+        # source: https://stackoverflow.com/a/23880531/3545896
+        # source: https://stackoverflow.com/questions/15519749/how-to-get-widget-background-qcolor
+        self.scene.setBackgroundBrush(app.palette().color(QPalette.Window))
+
+        self.update_drawn_pages()
+
+    def launch_auto_zoom(self):
+        """Zoom in a regular interval.
+
+        Used to demonstrate zooming only, should be removed once the project
+        matures.
+        """
+        def do_on_timeout():
+            self.zoom(1 / 1.00005)
+
+        timer = QTimer(self)
+        timer.timeout.connect(do_on_timeout)
+        timer.start(10)
+
+    def zoom(self, zoom_factor):
+        """Zoom GraphicsView by zoom_factor.
+
+        source: https://stackoverflow.com/a/29026916/3545896
+
+        Parameters
+        ----------
+        zoom_factor : float
+            > 1 to zoom in and < 1 to zoom out
+        """
+        # Set Anchors
+        self.view.setTransformationAnchor(QGraphicsView.NoAnchor)
+        self.view.setResizeAnchor(QGraphicsView.NoAnchor)
+
+        # Zoom
+        self.view.scale(zoom_factor, zoom_factor)
+
+        pos_old = self.view.mapToScene(QPoint(self.soi.WIDTH / 2,
+                                              self.soi.HEIGHT / 2))
+        pos_new = self.view.mapToScene(self.soi.WIDTH / 2, self.soi.HEIGHT / 2)
+        delta = pos_new - pos_old
+        self.view.translate(delta.x(), delta.y())
 
 
 if __name__ == "__main__":
     app = QApplication(sys.argv)
     WINDOW = MainWindow()
-    WINDOW.show()
+    WINDOW.showMaximized()
     app.exec_()
diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py
index 92e7e6bc6145a1a1921cb5580c7b8384d77cad51..630cdaeb4c5e7b982b52b49413cbde406abedb74 100644
--- a/soitool/modules/module_base.py
+++ b/soitool/modules/module_base.py
@@ -9,6 +9,10 @@ class ModuleBase(ABC):
         """Abstract method, should be implemented by derived class."""
         raise NotImplementedError
 
+    def set_pos(self, pos):
+        """Abstract method, should be implemented by derived class."""
+        raise NotImplementedError
+
     def render_onto_pdf(self):
         """Abstract method, should be implemented by derived class."""
         raise NotImplementedError
diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py
index 6ddf18b4645cd7e18294adb1bc9d518676b0e39b..e9d06a7502c15d4d859c0173eb39ccc7de5dd481 100644
--- a/soitool/modules/module_table.py
+++ b/soitool/modules/module_table.py
@@ -27,6 +27,10 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
     Has shortcuts for adding and removing rows and columns.
     """
 
+    def set_pos(self, pos):
+        """Set position of widget."""
+        self.move(pos)
+
     def __init__(self):
         QTableWidget.__init__(self)
         super(QTableWidget)
diff --git a/test/test_main.py b/test/test_main.py
index beadccfe5991936ceba67b155d69db315ddf1414..4f70f155c05aa9d0baf3e7f0143110aa4e3e7981 100644
--- a/test/test_main.py
+++ b/test/test_main.py
@@ -59,13 +59,19 @@ class TestMain(unittest.TestCase):
         """Test at endring av tekst funker."""
 
         def change_text_and_ok():
-            while self.widget.dlg_input is None:
-                app.processEvents()
-            child_line_edit = self.widget.dlg_input.findChild(QtWidgets.QLineEdit)
+            # in PySide2 we need to store a reference to this. If we don't the
+            # widget is garbage collected somehow before we get to use
+            # child_line_edit (a child of the active widget)
+            active_widget = app.activeModalWidget()
+
+            child_line_edit = active_widget.findChild(
+                QtWidgets.QLineEdit
+            )
+
             QtTest.QTest.keyClicks(child_line_edit, self.test_text2)
             QtTest.QTest.keyClick(child_line_edit, QtCore.Qt.Key_Enter)
 
-        QtCore.QTimer.singleShot(100, change_text_and_ok)
+        QtCore.QTimer.singleShot(0, change_text_and_ok)
         QtTest.QTest.mouseClick(self.widget.button, QtCore.Qt.LeftButton)
 
         self.assertEqual(