diff --git a/Makefile b/Makefile
index 4610651a4339b3433e63fd3897a3eb289e55c5a0..a4760cf266c78f921167ea1205d0140dffb287a9 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,7 @@ teste2e:
 	cd frontend && yarn build && cp dist/index.html ../core/templates && cd .. && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest  --ds=grady.settings $(path); git checkout core/templates/index.html
 
 teste2e-nc:
-	cp frontend/dist/index.html ./core/templates && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest -n 4 --ds=grady.settings $(path); git checkout core/templates/index.html
+	cp frontend/dist/index.html ./core/templates && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest --ds=grady.settings $(path); git checkout core/templates/index.html
 
 
 coverage:
diff --git a/core/migrations/0016_solutioncomment.py b/core/migrations/0016_solutioncomment.py
new file mode 100644
index 0000000000000000000000000000000000000000..d76621d464405ace6b2a55f718b46dd6a56ed9b5
--- /dev/null
+++ b/core/migrations/0016_solutioncomment.py
@@ -0,0 +1,27 @@
+# Generated by Django 2.1.4 on 2019-05-14 15:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0015_feedbacklabel_colour'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SolutionComment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('text', models.TextField()),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('modified', models.DateTimeField(auto_now=True)),
+                ('of_line', models.PositiveIntegerField()),
+                ('of_submission_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to='core.SubmissionType')),
+                ('of_user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+    ]
diff --git a/core/migrations/0021_merge_20190902_1246.py b/core/migrations/0021_merge_20190902_1246.py
new file mode 100644
index 0000000000000000000000000000000000000000..aba1adb9618428670de6e81ec9c8c324d5e4709b
--- /dev/null
+++ b/core/migrations/0021_merge_20190902_1246.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.1.11 on 2019-09-02 12:46
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0016_solutioncomment'),
+        ('core', '0020_auto_20190831_1417'),
+    ]
+
+    operations = [
+    ]
diff --git a/core/models/__init__.py b/core/models/__init__.py
index 2b5cb5663f4e95ca09290c67e9e6c3dbfe6e7838..47ab0c28db6506e4a7cd3ceadcf0c5c7b3817354 100644
--- a/core/models/__init__.py
+++ b/core/models/__init__.py
@@ -1,5 +1,6 @@
 from .exam_type import ExamType  # noqa
-from .submission_type import SubmissionType  # noqa
+from .submission_type import SubmissionType, SolutionComment  # noqa
+from .user_account import UserAccount, TutorReviewerManager  # noqa
 from .user_account import UserAccount, TutorReviewerManager  # noqa
 from .student_info import StudentInfo, random_matrikel_no  # noqa
 from .test import Test  # noqa
diff --git a/core/models/submission_type.py b/core/models/submission_type.py
index 7c2dc3c9f8e56ab2a572eb2c174f3ca2125bf619..feb93e082e8a9f34aa5f97405644b515281db29b 100644
--- a/core/models/submission_type.py
+++ b/core/models/submission_type.py
@@ -98,3 +98,21 @@ class SubmissionType(models.Model):
                 ),
                 submission_count=Count('submissions'),
             ).order_by('name')
+
+
+class SolutionComment(models.Model):
+    text = models.TextField()
+    created = models.DateTimeField(auto_now_add=True)
+    modified = models.DateTimeField(auto_now=True)
+
+    of_line = models.PositiveIntegerField()
+    of_user = models.ForeignKey(
+        'UserAccount',
+        related_name="solution_comments",
+        on_delete=models.PROTECT
+    )
+    of_submission_type = models.ForeignKey(
+        SubmissionType,
+        related_name="solution_comments",
+        on_delete=models.PROTECT,
+    )
diff --git a/core/serializers/__init__.py b/core/serializers/__init__.py
index f7e15a2a9bf77bfbac977f85490bad6e8c236cba..2f69017b326d3761bc3aa2132b0a95687bd90e68 100644
--- a/core/serializers/__init__.py
+++ b/core/serializers/__init__.py
@@ -1,4 +1,6 @@
 from .common_serializers import *  # noqa
+from .submission_type import (SubmissionTypeListSerializer, SubmissionTypeSerializer,  # noqa
+                              SolutionCommentSerializer)  # noqa
 from .feedback import (FeedbackSerializer, FeedbackCommentSerializer,  # noqa
                        VisibleCommentFeedbackSerializer)  # noqa
 from .subscription import *  # noqa
diff --git a/core/serializers/common_serializers.py b/core/serializers/common_serializers.py
index 305b2c6cb2cad6f368cc547f9ea9ee00d210a686..94e22592b3b4f93734607b21f82092f6cde5d63c 100644
--- a/core/serializers/common_serializers.py
+++ b/core/serializers/common_serializers.py
@@ -1,8 +1,11 @@
 import logging
+from collections import defaultdict
 
 import django.contrib.auth.password_validation as validators
 from django.core import exceptions
+from django.db.models.manager import Manager
 from rest_framework import serializers
+from rest_framework.utils import html
 
 from core import models
 
@@ -26,25 +29,6 @@ class TestSerializer(DynamicFieldsModelSerializer):
         fields = ('pk', 'name', 'label', 'annotation')
 
 
-class SubmissionTypeListSerializer(DynamicFieldsModelSerializer):
-
-    class Meta:
-        model = models.SubmissionType
-        fields = ('pk', 'name', 'full_score')
-
-
-class SubmissionTypeSerializer(SubmissionTypeListSerializer):
-
-    class Meta:
-        model = models.SubmissionType
-        fields = ('pk',
-                  'name',
-                  'full_score',
-                  'description',
-                  'solution',
-                  'programming_language')
-
-
 class UserAccountSerializer(DynamicFieldsModelSerializer):
 
     def validate(self, data):
@@ -63,3 +47,49 @@ class UserAccountSerializer(DynamicFieldsModelSerializer):
         fields = ('pk', 'username', 'role', 'is_admin', 'password')
         read_only_fields = ('pk', 'username', 'role', 'is_admin')
         extra_kwargs = {'password': {'write_only': True}}
+
+
+class CommentDictionarySerializer(serializers.ListSerializer):
+
+    def to_internal_value(self, comment_dict):
+        """ Converts a line_no -> comment list dictionary back to a list
+        of comments. Currently we do not have any information about the
+        feedback since it is not available in this scope. Feedback is
+        responsible to add it later on update/creation """
+        if html.is_html_input(comment_dict):
+            comment_dict = html.parse_html_list(comment_dict)
+
+        if not isinstance(comment_dict, dict):
+            raise serializers.ValidationError(
+                'Comments have to be provided as a dict'
+                'with: line -> list of comments'
+            )
+
+        ret = []
+        errors = []
+
+        for line, comment in comment_dict.items():
+            try:
+                comment['of_line'] = line
+                validated = self.child.run_validation(comment)
+            except serializers.ValidationError as err:
+                errors.append(err.detail)
+            else:
+                ret.append(validated)
+                errors.append({})
+
+        if any(errors):
+            raise serializers.ValidationError(errors)
+
+        return ret
+
+    def to_representation(self, comments):
+        """ Provides a dict where all the keys correspond to lines and contain
+        a list of comments on that line. """
+        if isinstance(comments, Manager):
+            comments = comments.all()
+
+        ret = defaultdict(list)
+        for comment in comments:
+            ret[comment.of_line].append(self.child.to_representation(comment))
+        return ret
diff --git a/core/serializers/feedback.py b/core/serializers/feedback.py
index cccfa16d400cc3da20a780d41e016f4a930eb92b..6b0e128814d16a094b04fb4df153110156cf9741 100644
--- a/core/serializers/feedback.py
+++ b/core/serializers/feedback.py
@@ -1,13 +1,11 @@
 import logging
-from collections import defaultdict
 
 from django.db import transaction
-from django.db.models.manager import Manager
 from rest_framework import serializers
-from rest_framework.utils import html
 
 from core import models
 from core.models import Feedback, UserAccount
+from core.serializers import CommentDictionarySerializer
 from util.factories import GradyUserFactory
 
 from .generic import DynamicFieldsModelSerializer
@@ -16,52 +14,6 @@ log = logging.getLogger(__name__)
 user_factory = GradyUserFactory()
 
 
-class FeedbackCommentDictionarySerializer(serializers.ListSerializer):
-
-    def to_internal_value(self, comment_dict):
-        """ Converts a line_no -> comment list dictionary back to a list
-        of comments. Currently we do not have any information about the
-        feedback since it is not availiable in this scope. Feedback is
-        responsible to add it later on update/creation """
-        if html.is_html_input(comment_dict):
-            comment_dict = html.parse_html_list(comment_dict)
-
-        if not isinstance(comment_dict, dict):
-            raise serializers.ValidationError(
-                'Comments have to be provided as a dict'
-                'with: line -> list of comments'
-            )
-
-        ret = []
-        errors = []
-
-        for line, comment in comment_dict.items():
-            try:
-                comment['of_line'] = line
-                validated = self.child.run_validation(comment)
-            except serializers.ValidationError as err:
-                errors.append(err.detail)
-            else:
-                ret.append(validated)
-                errors.append({})
-
-        if any(errors):
-            raise serializers.ValidationError(errors)
-
-        return ret
-
-    def to_representation(self, comments):
-        """ Provides a dict where all the keys correspond to lines and contain
-        a list of comments on that line. """
-        if isinstance(comments, Manager):
-            comments = comments.all()
-
-        ret = defaultdict(list)
-        for comment in comments:
-            ret[comment.of_line].append(self.child.to_representation(comment))
-        return ret
-
-
 class FeedbackCommentSerializer(DynamicFieldsModelSerializer):
     of_tutor = serializers.StringRelatedField(source='of_tutor.username')
     labels = serializers.PrimaryKeyRelatedField(many=True, required=False,
@@ -84,7 +36,7 @@ class FeedbackCommentSerializer(DynamicFieldsModelSerializer):
             'of_feedback': {'write_only': True},
             'of_line': {'write_only': True},
         }
-        list_serializer_class = FeedbackCommentDictionarySerializer
+        list_serializer_class = CommentDictionarySerializer
 
 
 class FeedbackSerializer(DynamicFieldsModelSerializer):
@@ -99,7 +51,7 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
         """ Search for the assignment of this feedback and report in which
         stage the tutor has worked on it.
 
-        Note: This method is unorthodox since it mingles the rather dump
+        TODO Note: This method is unorthodox since it mingles the rather dump
         feedback object with assignment logic. The reverse lookups in the
         method are not pre-fetched. Remove if possible. """
         if 'request' not in self.context:
diff --git a/core/serializers/submission_type.py b/core/serializers/submission_type.py
new file mode 100644
index 0000000000000000000000000000000000000000..b924c578e0526ecd621766596784e4c6ba95b4dc
--- /dev/null
+++ b/core/serializers/submission_type.py
@@ -0,0 +1,65 @@
+import logging
+
+from rest_framework import serializers
+from rest_framework.exceptions import ValidationError
+
+from core import models
+from core.serializers import DynamicFieldsModelSerializer, CommentDictionarySerializer
+
+log = logging.getLogger(__name__)
+
+
+class SolutionCommentSerializer(DynamicFieldsModelSerializer):
+    of_user = serializers.StringRelatedField(source='of_user.username')
+
+    def validate(self, attrs):
+        super().validate(attrs)
+        submission_type = attrs.get('of_submission_type')
+        of_line = attrs.get('of_line')
+        if self.instance:
+            submission_type = self.instance.of_submission_type
+            of_line = self.instance.of_line
+
+        max_line_number = len(submission_type.solution.split('\n'))
+
+        if not (0 < of_line <= max_line_number):
+            raise ValidationError('Invalid line number for comment')
+        return attrs
+
+    def create(self, validated_data):
+        validated_data['of_user'] = self.context['request'].user
+        return super().create(validated_data)
+
+    class Meta:
+        model = models.SolutionComment
+        fields = (
+            'pk',
+            'text',
+            'created',
+            'of_user',
+            'of_line',
+            'of_submission_type'
+        )
+        read_only_fields = ('pk', 'created', 'of_user')
+        list_serializer_class = CommentDictionarySerializer
+
+
+class SubmissionTypeListSerializer(DynamicFieldsModelSerializer):
+
+    class Meta:
+        model = models.SubmissionType
+        fields = ('pk', 'name', 'full_score')
+
+
+class SubmissionTypeSerializer(DynamicFieldsModelSerializer):
+    solution_comments = SolutionCommentSerializer(many=True, required=False)
+
+    class Meta:
+        model = models.SubmissionType
+        fields = ('pk',
+                  'name',
+                  'full_score',
+                  'description',
+                  'solution',
+                  'programming_language',
+                  'solution_comments')
diff --git a/core/urls.py b/core/urls.py
index aee6e268814e22e9ef38d5f84e62f158ec4ec89f..25fad50d86185a3e09ee8d545a513f0b8b0ca597 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -25,6 +25,7 @@ router.register('statistics', views.StatisticsEndpoint, basename='statistics')
 router.register('user', views.UserAccountViewSet, basename='user')
 router.register('label', views.LabelApiViewSet, basename='label')
 router.register('label-statistics', views.LabelStatistics, basename='label-statistics')
+router.register('solution-comment', views.SolutionCommentApiViewSet, basename='solution-comment')
 
 schema_view = get_schema_view(
     openapi.Info(
diff --git a/core/views/common_views.py b/core/views/common_views.py
index 99d3988b18dcc694326a30a884c7735522727ab1..3acd7245da15fdece8ad6be73e0465eb7934ed0a 100644
--- a/core/views/common_views.py
+++ b/core/views/common_views.py
@@ -24,7 +24,7 @@ from core.serializers import (ExamSerializer, StudentInfoSerializer,
                               StudentInfoForListViewSerializer,
                               SubmissionNoTypeSerializer, StudentSubmissionSerializer,
                               SubmissionTypeSerializer, CorrectorSerializer,
-                              UserAccountSerializer)
+                              UserAccountSerializer, SolutionCommentSerializer)
 
 log = logging.getLogger(__name__)
 
@@ -130,6 +130,29 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
     permission_classes = (IsTutorOrReviewer, )
 
 
+class SolutionCommentApiViewSet(
+        mixins.CreateModelMixin,
+        mixins.UpdateModelMixin,
+        mixins.DestroyModelMixin,
+        viewsets.GenericViewSet):
+    permission_classes = (IsTutorOrReviewer, )
+    queryset = models.SolutionComment.objects.all()
+    serializer_class = SolutionCommentSerializer
+
+    def destroy(self, request, *args, **kwargs):
+        instance = self.get_object()
+        if not request.user.is_reviewer() and instance.of_user != request.user:
+            raise PermissionDenied(detail="You can only delete comments you made")
+        self.perform_destroy(instance)
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+    def update(self, request, *args, **kwargs):
+        instance = self.get_object()
+        if instance.of_user != request.user:
+            raise PermissionDenied(detail="You can only update comments you made")
+        return super().update(request, *args, **kwargs)
+
+
 class StatisticsEndpoint(viewsets.ViewSet):
     permission_classes = (IsTutorOrReviewer, )
 
diff --git a/core/views/export.py b/core/views/export.py
index 9e5f91a916173e8f727f6a082daefbba7483d227..307fecbed4529809e61c6614e50ab0a2d087859e 100644
--- a/core/views/export.py
+++ b/core/views/export.py
@@ -7,7 +7,7 @@ import xkcdpass.xkcd_password as xp
 
 from core.models import StudentInfo, UserAccount, ExamType, SubmissionType
 from core.permissions import IsReviewer
-from core.serializers.common_serializers import SubmissionTypeSerializer, \
+from core.serializers import SubmissionTypeSerializer, \
     ExamSerializer, UserAccountSerializer
 from core.serializers.student import StudentExportSerializer
 from core.serializers.tutor import CorrectorSerializer
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 7d575c89f500a065f530c76224e6692c6baca091..ec0687b77abfcf45b942b54b93160be9c69683e9 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -12,7 +12,7 @@ import {
   SubmissionNoType, SubmissionType,
   Subscription,
   Tutor, UserAccount, LabelStatisticsForSubType,
-  FeedbackLabel,
+  FeedbackLabel, SolutionComment,
   CreateUpdateFeedback
 } from '@/models'
 
@@ -171,6 +171,26 @@ export async function fetchSubmissionTypes (): Promise<Array<SubmissionType>> {
   return (await ax.get(url)).data
 }
 
+export async function fetchSubmissionType (pk: string): Promise<SubmissionType> {
+  const url = `/api/submissiontype/${pk}`
+  return (await ax.get(url)).data
+}
+
+export async function deleteSolutionComment (pk: number): Promise<AxiosResponse<void>> {
+  const url = `/api/solution-comment/${pk}/`
+  return ax.delete(url)
+}
+
+export async function createSolutionComment(comment: Partial<SolutionComment>): Promise<SolutionComment> {
+  const url = `/api/solution-comment/`
+  return (await ax.post(url, comment)).data
+}
+
+export async function patchSolutionComment(comment: Partial<SolutionComment>): Promise<SolutionComment> {
+  const url = `/api/solution-comment/${comment.pk}/`
+  return (await ax.patch(url, comment)).data
+}
+
 export async function fetchAllAssignments (): Promise<Array<Assignment>> {
   const url = '/api/assignment/'
   return (await ax.get(url)).data
diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
index 150e8929c32f9bd8a03bc7f9de59b3767ca3fd44..0ad5e275b9cab80c498915ec9ac7058187214fdc 100644
--- a/frontend/src/components/submission_notes/SubmissionCorrection.vue
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -186,7 +186,7 @@ export default {
 
       const hasUpdatedComment = this.updatedFeedback && this.updatedFeedback[lineNo]
 
-      return !this.showFeedback && (hasOrigComment || hasUpdatedComment)
+      return !this.showFeedback && (hasOrigComment || !!hasUpdatedComment)
     },
     init () {
       SubmissionNotes.RESET_STATE()
diff --git a/frontend/src/components/submission_notes/base/SubmissionLine.vue b/frontend/src/components/submission_notes/base/SubmissionLine.vue
index 81c46f6d4b0ba8bf065a5620b8c9bc91d5928eaa..463e0c9c6a05b3e4f7e4981834e0c04f40336f42 100644
--- a/frontend/src/components/submission_notes/base/SubmissionLine.vue
+++ b/frontend/src/components/submission_notes/base/SubmissionLine.vue
@@ -1,17 +1,9 @@
 <template>
   <div>
-    <td class="line-number-cell">
-      <v-btn v-if="hint"
-        block
-        depressed
-        class="line-number-btn"
-        color="error"
-        @click="toggleEditor"
-      >
-        {{ lineNo }}
-      </v-btn>
+    <td
+      :style="backgroundColor"
+      class="line-number-cell">
       <v-btn
-        v-else
         flat
         block
         depressed
@@ -20,6 +12,7 @@
       >
         {{ lineNo }}
       </v-btn>
+      </v-btn>
     </td>
     <td class="code-cell-content pl-2">
         <span v-html="code" class="code-line"></span>
@@ -49,6 +42,11 @@ export default {
       default: false,
     },
   },
+  computed: {
+    backgroundColor() {
+      return this.hint ? 'background-color: #F44336;' : 'background-color: transparent;'
+    }
+  },
   methods: {
     toggleEditor () {
       this.$emit('toggleEditor')
diff --git a/frontend/src/components/SubmissionType.vue b/frontend/src/components/submission_type/SubmissionType.vue
similarity index 71%
rename from frontend/src/components/SubmissionType.vue
rename to frontend/src/components/submission_type/SubmissionType.vue
index 86388a33e8c8f5bbdbb55056c7c161b86c80bd85..699bdb5821bee232638b1c4fd8e4c13fd8569ff0 100644
--- a/frontend/src/components/SubmissionType.vue
+++ b/frontend/src/components/submission_type/SubmissionType.vue
@@ -7,7 +7,16 @@
           v-for="(item, i) in typeItems"
           :key="i"
         >
-          <div slot="header"><b>{{ item.title }}</b></div>
+          <div slot="header">
+            <b>{{ item.title }}</b>
+            <v-btn
+              class="ml-5"
+              color="info"
+              flat
+              v-if="item.title == 'Solution'"
+              @click.stop="showSolutionComments = !showSolutionComments"
+            >Toggle Comments</v-btn>
+          </div>
           <v-card
             v-if="item.title === 'Description'"
             class="type-description"
@@ -19,10 +28,14 @@
             </v-card-text>
           </v-card>
           <div v-else-if="item.title === 'Solution'">
-            <pre
-              class="elevation-2 solution-code pl-2"
-              :class="programmingLanguage"
-            ><span v-html="highlightedSolution"></span></pre>
+            <solution
+              :pk=pk
+              :solution=solution
+              :programmingLanguage=programmingLanguage
+              :solutionComments=solutionComments
+              :showSolutionComments=showSolutionComments
+            >
+            </solution>
           </div>
         </v-expansion-panel-content>
       </v-expansion-panel>
@@ -36,9 +49,17 @@ import Component from 'vue-class-component'
 import { Prop } from 'vue-property-decorator'
 import { highlight } from 'highlight.js'
 import { UI } from '@/store/modules/ui'
+import { SolutionComment } from '../../models';
+import Solution from '@/components/submission_type/solution/Solution.vue'
 
-@Component
+@Component({
+  components: {Solution}
+})
 export default class SubmissionType extends Vue {
+  @Prop({
+    type: String,
+    required: true,
+  }) pk!: string
   @Prop({
     type: String,
     required: true
@@ -64,6 +85,10 @@ export default class SubmissionType extends Vue {
     type: Boolean,
     default: false
   }) reverse!: boolean
+  @Prop({
+    type: Object,
+    default: {},
+  }) solutionComments!: {[ofLine: number]: SolutionComment[]}
   @Prop({
     type: Object,
     default: function () {
@@ -78,6 +103,8 @@ export default class SubmissionType extends Vue {
     ? [this.expandedByDefault.Description, this.expandedByDefault.Solution]
     : [this.expandedByDefault.Solution, this.expandedByDefault.Description]
 
+  showSolutionComments = true
+
   get typeItems () {
     let items = [
       {
@@ -107,10 +134,6 @@ export default class SubmissionType extends Vue {
 </script>
 
 <style>
-  .solution-code {
-    border-width: 0px;
-    white-space: pre-wrap;
-  }
   .type-description code {
     background-color: lightgrey;
   }
diff --git a/frontend/src/components/SubmissionTypesOverview.vue b/frontend/src/components/submission_type/SubmissionTypesOverview.vue
similarity index 72%
rename from frontend/src/components/SubmissionTypesOverview.vue
rename to frontend/src/components/submission_type/SubmissionTypesOverview.vue
index 88efc529d2985040292e18018de7e59335306844..2e8bc31b7ef5ae1112bfb6d11e0b91a2d62312ef 100644
--- a/frontend/src/components/SubmissionTypesOverview.vue
+++ b/frontend/src/components/submission_type/SubmissionTypesOverview.vue
@@ -3,7 +3,7 @@
     <v-card-title class="title">Task types</v-card-title>
       <v-layout row wrap>
           <v-flex xs3>
-              <v-list>
+              <v-list id="submission-types-list">
                   <v-list-tile
                           v-for="submissionType in sortedSubmissionTypes" :key="submissionType.pk"
                           @click="selectedSubmissionType = submissionType"
@@ -25,20 +25,29 @@
 </template>
 
 <script>
-import { mapState } from 'vuex'
-import SubmissionType from '@/components/SubmissionType'
+import SubmissionType from '@/components/submission_type/SubmissionType'
+import store from '@/store/store';
 export default {
   name: 'SubmissionTypesOverview',
   components: { SubmissionType },
   data () {
     return {
-      selectedSubmissionType: null
+      selectedSubmissionTypePk: null
     }
   },
   computed: {
-    ...mapState([
-      'submissionTypes'
-    ]),
+    submissionTypes () {
+      return store.state.submissionTypes
+    },
+    // needed to keep selectedSubmissionType reactive
+    selectedSubmissionType: {
+      get: function () {
+        return store.state.submissionTypes[this.selectedSubmissionTypePk]
+      },
+      set: function (newSubType) {
+        this.selectedSubmissionTypePk = newSubType.pk
+      }
+    },
     sortedSubmissionTypes () {
       return Object.values(this.submissionTypes).sort((t1, t2) => {
         let lowerName1 = t1.name.toLowerCase()
diff --git a/frontend/src/components/submission_type/solution/Solution.vue b/frontend/src/components/submission_type/solution/Solution.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ae4f1bd9a681818a177167f5b6fe4a20be3fd123
--- /dev/null
+++ b/frontend/src/components/submission_type/solution/Solution.vue
@@ -0,0 +1,190 @@
+<template>
+  <table class="solution-table">
+    <tr v-for="(code, lineNo) in highlightedSolution" :key="`${pk}:${lineNo}`" :id="`solution-line-${lineNo}`">
+      <td class="line-number-cell" :style="backgroundColor(lineNo)">
+        <v-btn
+        flat
+        block
+        depressed
+        class="line-number-btn"
+        @click="toggleEditor(lineNo)">{{lineNo}}</v-btn>
+      </td>
+      <td class="code-cell-content pl-2">
+        <span v-html="code" class="code-line"></span>
+        <template
+        v-if="solutionComments[lineNo] && solutionComments[lineNo].length && showSolutionComments">
+          <solution-comment
+            v-for="comment in solutionComments[lineNo]"
+            v-bind="comment"
+            :key="comment.pk"
+            @update-submission-type="updateSubmissionType"
+            @toggle-editor="toggleEditor(lineNo)"
+            @toggle-eidt-editor="toggleEditor(lineNo)"
+          />
+        </template>
+        <template v-if="showEditorOnline[lineNo]">
+          <v-textarea
+            name="solution-comment-input"
+            label="Here you can comment the solution. Other tutors will see those comments."
+            v-model="editedSolutionComments[lineNo]"
+            @keyup.enter.ctrl.exact="submitSolutionComment(lineNo)"
+            @keyup.esc="collapseTextField(lineNo)"
+            @focus="selectInput($event)"
+            rows="2"
+            outline
+            autofocus
+            auto-grow
+            hide-details
+            class="mx-2"
+          />
+          <v-btn id="submit-comment" color="success" @click="submitSolutionComment(lineNo)"><v-icon>check</v-icon>Submit</v-btn>
+          <v-btn id="cancel-comment" @click="toggleEditor(lineNo)"><v-icon>cancel</v-icon>cancel</v-btn>
+        </template>
+      </td>
+    </tr>
+  </table>
+</template>
+
+<script lang="ts">
+  import { Vue, Component, Prop } from "vue-property-decorator"
+  import { SolutionComment, FeedbackComment } from "../../../models"
+  import { highlight } from "highlight.js"
+  import { syntaxPostProcess, objectifyArray } from "../../../util/helpers"
+  import SolutionCommentComponent from "@/components/submission_type/solution/SolutionComment.vue"
+  import * as api from '@/api'
+  import { actions } from '../../../store/actions';
+
+  @Component({
+    components: {'SolutionComment': SolutionCommentComponent}
+  })
+  export default class Solution extends Vue {
+    @Prop({
+      type: String,
+      required: true
+    })
+    pk!: string
+    @Prop({
+      type: String,
+      required: false,
+      default: ""
+    })
+    solution!: string
+    @Prop({
+      type: String,
+      default: "c"
+    })
+    programmingLanguage!: string
+    @Prop({
+      type: Object,
+      default: {}
+    })
+    solutionComments!: { [ofLine: number]: SolutionComment[] }
+    @Prop({
+      type: Boolean,
+      default: true
+    })
+    showSolutionComments!: boolean
+
+
+    timer = 0
+    showEditorOnline: {[ofLine: number]: boolean} = {}
+    editedSolutionComments: {[ofLine: number]: string} = {}
+
+    get highlightedSolution() {
+      const highlighted = highlight(this.programmingLanguage, this.solution, true)
+        .value
+      const postprocessed = syntaxPostProcess(highlighted)
+      return postprocessed
+        .split("\n")
+        .reduce((acc: { [k: number]: string }, curr, index) => {
+          acc[index + 1] = curr
+          return acc
+        }, {})
+    }
+
+    get lineNoHint() {
+      if (this.showSolutionComments) {
+        // will return a falsy value if indexed with a line number
+        // meaning no hint will be displayed
+        return {}
+      } else {
+        // returning the solutionComments will return a truthy value
+        // if indexed with the line number where comments are located
+        return this.solutionComments
+      }
+    }
+
+    backgroundColor(lineNo: number) {
+      if (this.lineNoHint[lineNo]) {
+        return 'backgroundColor: #64B5F6;'
+      } else {
+        'backgroundColor: transparent;'
+      }
+    }
+
+    selectInput (event: Event) {
+      if (event !== null) {
+        const target = event.target as HTMLTextAreaElement
+        target.select()
+      }
+    }
+
+    toggleEditor(lineNo: number) {
+      Vue.set(this.showEditorOnline, lineNo, !this.showEditorOnline[lineNo])
+    }
+
+    async submitSolutionComment(lineNo: number) {
+      const comment = {
+        text: this.editedSolutionComments[lineNo],
+        ofLine: lineNo,
+        ofSubmissionType: this.pk
+      }
+      await api.createSolutionComment(comment)
+      await actions.updateSubmissionType(this.pk)
+      this.toggleEditor(lineNo)
+      this.editedSolutionComments[lineNo] = ''
+    }
+
+    updateSubmissionType() {
+      actions.updateSubmissionType(this.pk)
+    }
+
+    mounted() {
+      this.timer = setInterval(() => {
+        actions.updateSubmissionType(this.pk)
+      }, 10 * 1e3)
+    }
+
+    beforeDestroy() {
+      clearInterval(this.timer)
+    }
+  }
+</script>
+
+<style scoped>
+  .solution-table {
+    table-layout: auto;
+    border-collapse: collapse;
+    width: 100%;
+  }
+
+  .line-number-cell {
+    vertical-align: top
+  }
+
+  .code-cell-content {
+    width: 100%
+  }
+
+  .code-line {
+    white-space: pre-wrap;
+    font-family: monospace;
+  }
+
+  .line-number-btn {
+    height: fit-content;
+    min-width: 50px;
+    margin: 0;
+    border-radius: 0px;
+  }
+</style>
diff --git a/frontend/src/components/submission_type/solution/SolutionComment.vue b/frontend/src/components/submission_type/solution/SolutionComment.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ef5b9a9fdec3e116c88fd589044bcea638d0737d
--- /dev/null
+++ b/frontend/src/components/submission_type/solution/SolutionComment.vue
@@ -0,0 +1,197 @@
+<template>
+  <div>
+    <div class="dialog-box" @click="$emit('toggle-editor')">
+      <div class="body elevation-1" :style="{borderColor: '#3D8FC1', backgroundColor}">
+        <span class="tip tip-up" :style="{borderBottomColor: '#3D8FC1'}"></span>
+        <span v-if="ofUser" class="of-user">Of user: {{ofUser}}</span>
+        <span class="comment-created">{{parsedCreated}}</span>
+        <div class="message">{{text}}</div>
+        <v-btn
+          flat icon absolute
+          class="delete-button"
+          v-if="deletable"
+          @click.stop="deleteConfirmation = true"
+        >
+          <v-icon color="grey darken-1" size="20px">delete_forever</v-icon>
+        </v-btn>
+        <v-btn
+          flat icon absolute
+          class="edit-button"
+          v-if="editable"
+          @click.stop="toggleEditing()"
+        >
+          <v-icon color="grey darken-1" size="20px">edit</v-icon>
+        </v-btn>
+      </div>
+    </div>
+    <template v-if="editing">
+      <v-textarea
+        name="solution-comment-edit"
+        label="Here you can edit your comment"
+        v-model="editedText"
+        @keyup.enter.ctrl.exact="submitEdit"
+        @keyup.esc="editing = false"
+        @focus="selectInput($event)"
+        rows="2"
+        outline
+        autofocus
+        auto-grow
+        hide-details
+        class="mx-2"
+      />
+      <v-btn id="submit-comment" color="success" @click="submitEdit"><v-icon>check</v-icon>Submit</v-btn>
+      <v-btn id="cancel-comment" @click="editing = false"><v-icon>cancel</v-icon>cancel</v-btn>
+    </template>
+
+    <v-dialog
+      v-model="deleteConfirmation"
+      max-width="max-content"
+    >
+      <v-card
+      class="text-xs-center pa-2"
+      >
+        <v-card-title class="title">
+          Delete permanently?
+        </v-card-title>
+        <v-card-actions>
+          <v-btn :id="`confirm-delete-comment`" color="red lighten-1" @click="deleteComment">delete</v-btn>
+          <v-btn @click="deleteConfirmation = false">cancel</v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </div>
+</template>
+
+<script lang="ts">
+import {Vue, Component, Prop, Provide} from 'vue-property-decorator'
+import { UI } from '@/store/modules/ui'
+import { SubmissionNotes } from '@/store/modules/submission-notes'
+import { Authentication } from '../../../store/modules/authentication';
+import * as api from "@/api"
+import { actions } from '@/store/actions';
+
+@Component
+export default class SolutionComment extends Vue {
+  @Prop({
+    type: Number,
+    required: true
+  }) pk!: number
+  @Prop({
+    type: String,
+    required: true
+  }) text!: string
+  @Prop({
+    type: String,
+    required: false
+  }) created?: string
+  @Prop({
+    type: String,
+    required: true
+  }) ofUser!: string
+  @Prop({
+    type: Number,
+    required: true
+  }) ofLine!: number
+
+  editing: boolean = false
+  editedText: string = ''
+  deleteConfirmation: boolean =  false
+
+  get parsedCreated() {
+    if (this.created) {
+      return new Date(this.created).toLocaleString()
+    } else {
+      return 'Just now'
+    }
+  }
+
+  get backgroundColor () {
+    return UI.state.darkMode ? 'grey' : '#F3F3F3'
+  }
+
+  get deletable() {
+    return Authentication.state.user.username === this.ofUser || Authentication.isReviewer
+  }
+
+  get editable() {
+    return Authentication.state.user.username === this.ofUser
+  }
+
+  toggleEditing() {
+    console.log('adasd')
+    this.editing = !this.editing
+    this.editedText = this.text
+  }
+
+  async deleteComment() {
+    await api.deleteSolutionComment(this.pk)
+    this.$emit('update-submission-type')
+  }
+
+  async submitEdit() {
+    await api.patchSolutionComment({pk: this.pk, text: this.editedText})
+    this.editing = false
+    this.$emit('update-submission-type')
+  }
+
+  selectInput (event: Event) {
+    if (event !== null) {
+      const target = event.target as HTMLTextAreaElement
+      target.select()
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .tip {
+    width: 0px;
+    height: 0px;
+    position: absolute;
+    background: transparent;
+    border: 10px solid;
+  }
+  .tip-up {
+    top: -22px; /* Same as body margin top + border */
+    left: 10px;
+    border-right-color: transparent;
+    border-left-color: transparent;
+    border-top-color: transparent;
+  }
+  .dialog-box .body {
+    cursor: pointer;
+    position: relative;
+    height: auto;
+    margin: 20px 10px 10px 10px;
+    padding: 5px;
+    border-radius: 0px;
+    border: 2px solid;
+  }
+  .body .message {
+    min-height: 30px;
+    border-radius: 3px;
+    font-size: 14px;
+    line-height: 1.5;
+    white-space: pre-wrap;
+  }
+  .delete-button {
+    bottom: -12px;
+    left: -42px;
+  }
+  .edit-button {
+    bottom: 15px;
+    left: -42px;
+  }
+  .comment-created {
+    position: absolute;
+    font-size: 10px;
+    right: 4px;
+    top: -20px;
+  }
+  .of-user {
+    position: absolute;
+    font-size: 13px;
+    top: -20px;
+    left: 50px;
+  }
+</style>
diff --git a/frontend/src/models.ts b/frontend/src/models.ts
index ffde4332511307816cf681382c4cf4216ef141f9..7a1ac897bb52862d9603de5bfcd4ce1f29b7a695 100644
--- a/frontend/src/models.ts
+++ b/frontend/src/models.ts
@@ -595,6 +595,17 @@ export interface SubmissionType {
      * @memberof SubmissionType
      */
     programmingLanguage?: SubmissionType.ProgrammingLanguageEnum
+
+    solutionComments: {[ofLine: number]: SolutionComment[]}
+}
+
+export interface SolutionComment {
+    pk: number,
+    created: string,
+    ofLine: number,
+    ofSubmissionType: string,
+    ofUser: string,
+    text: string
 }
 
 /**
diff --git a/frontend/src/pages/StudentSubmissionSideView.vue b/frontend/src/pages/StudentSubmissionSideView.vue
index d2859dad1a71d3421167b17467c3a38d484f4215..6f1ee7b8798f262368dac0398b4bed3d7da5616a 100644
--- a/frontend/src/pages/StudentSubmissionSideView.vue
+++ b/frontend/src/pages/StudentSubmissionSideView.vue
@@ -27,7 +27,7 @@ import store from '@/store/store'
 import VueInstance from '@/main'
 import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection'
 import SubmissionTests from '@/components/SubmissionTests'
-import SubmissionType from '@/components/SubmissionType'
+import SubmissionType from '@/components/submission_type/SubmissionType'
 import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation'
 import { actions } from '@/store/actions'
 
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 7040ba24629205a514b7b5fe6c7a40134ebe3375..fbfcc61b91a4e6701e216e588fd5a1ae92969826 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -36,7 +36,7 @@
 import { Vue, Component} from 'vue-property-decorator'
 import { Route, NavigationGuard } from 'vue-router'
 import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection.vue'
-import SubmissionType from '@/components/SubmissionType.vue'
+import SubmissionType from '@/components/submission_type/SubmissionType.vue'
 import store from '@/store/store'
 import { SubmissionNotes } from '@/store/modules/submission-notes'
 import SubmissionTests from '@/components/SubmissionTests.vue'
diff --git a/frontend/src/pages/reviewer/ReviewerStartPage.vue b/frontend/src/pages/reviewer/ReviewerStartPage.vue
index 0f9fe4e553c72a7c96dab1e33cb7543afcd37bc4..860c602f5b55b316184d779a49649257c67b7642 100644
--- a/frontend/src/pages/reviewer/ReviewerStartPage.vue
+++ b/frontend/src/pages/reviewer/ReviewerStartPage.vue
@@ -15,7 +15,7 @@
 <script>
 import CorrectionStatistics from '@/components/CorrectionStatistics'
 import SubscriptionList from '@/components/subscriptions/SubscriptionList'
-import SubmissionTypesOverview from '@/components/SubmissionTypesOverview'
+import SubmissionTypesOverview from '@/components/submission_type/SubmissionTypesOverview'
 
 export default {
   components: {
diff --git a/frontend/src/pages/student/StudentSubmissionPage.vue b/frontend/src/pages/student/StudentSubmissionPage.vue
index d7827230e36a20e76768e3e6efbcaa4c528bbd21..d5632bcdb18f35a123cd1d912d645dafc524be31 100644
--- a/frontend/src/pages/student/StudentSubmissionPage.vue
+++ b/frontend/src/pages/student/StudentSubmissionPage.vue
@@ -55,7 +55,7 @@
 <script>
 import { mapState, mapGetters } from 'vuex'
 import AnnotatedSubmission from '@/components/submission_notes/SubmissionCorrection'
-import SubmissionType from '@/components/SubmissionType'
+import SubmissionType from '@/components/submission_type/SubmissionType'
 import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission'
 import SubmissionLine from '@/components/submission_notes/base/SubmissionLine'
 import FeedbackComment from '@/components/submission_notes/base/FeedbackComment'
diff --git a/frontend/src/pages/tutor/TutorStartPage.vue b/frontend/src/pages/tutor/TutorStartPage.vue
index 94359ac71f2d3a4cc3d2d4047aea86c1a6c45490..ad127cf25452e2a726d53e885fe26afa22bbfb7d 100644
--- a/frontend/src/pages/tutor/TutorStartPage.vue
+++ b/frontend/src/pages/tutor/TutorStartPage.vue
@@ -15,7 +15,7 @@
 <script>
 import SubscriptionList from '@/components/subscriptions/SubscriptionList'
 import CorrectionStatistics from '@/components/CorrectionStatistics'
-import SubmissionTypesOverview from '@/components/SubmissionTypesOverview'
+import SubmissionTypesOverview from '@/components/submission_type/SubmissionTypesOverview'
 
 export default {
   components: {
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index ac0982b9246e39c57531345326c100f5b9f54173..459424280ae43dddc4dc9b3d45171529f6a6d042 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -16,14 +16,21 @@ async function getExamTypes (context: BareActionContext<RootState, RootState>) {
   const examTypes = await api.fetchExamTypes()
   mut.SET_EXAM_TYPES(examTypes)
 }
-async function updateSubmissionTypes (
-  context: BareActionContext<RootState, RootState>
-) {
+async function updateSubmissionTypes (){
   const submissionTypes = await api.fetchSubmissionTypes()
   submissionTypes.forEach(type => {
     mut.UPDATE_SUBMISSION_TYPE(type)
   })
 }
+
+async function updateSubmissionType (
+  context: BareActionContext<RootState, RootState>,
+  pk: string
+) {
+  const submissionType = await api.fetchSubmissionType(pk)
+  mut.UPDATE_SUBMISSION_TYPE(submissionType)
+}
+
 async function getStudents (
   context: BareActionContext<RootState, RootState>,
   opt: { studentPks: Array<string>} = {
@@ -85,6 +92,7 @@ const mb = getStoreBuilder<RootState>()
 
 export const actions = {
   updateSubmissionTypes: mb.dispatch(updateSubmissionTypes),
+  updateSubmissionType: mb.dispatch(updateSubmissionType),
   getExamTypes: mb.dispatch(getExamTypes),
   getStudents: mb.dispatch(getStudents),
   getSubmissionFeedbackTest: mb.dispatch(getSubmissionFeedbackTest),
diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts
index e65f67949745820e5ab989612baf3f2d4663e150..9b2335c73d2ef121bdf70b8a700ac120f0d29d14 100644
--- a/frontend/src/store/modules/submission-notes.ts
+++ b/frontend/src/store/modules/submission-notes.ts
@@ -70,7 +70,7 @@ const submissionGetter = mb.read(function submission(state, getters) {
     ? getters.submissionType.programmingLanguage
     : 'c'
   const highlighted = hljs.highlight(language, state.submission.text || '', true).value
-  const postProcessed = syntaxPostProcess(highlighted);
+  const postProcessed = syntaxPostProcess(highlighted)
   const splitted = postProcessed.split('\n').reduce((acc: { [k: number]: string }, cur, index) => {
     acc[index + 1] = cur
     return acc
diff --git a/frontend/src/store/mutations.ts b/frontend/src/store/mutations.ts
index 24bea75632ec7fbc8bd0d82350d7e4077ab62338..386a9df898b45c0c65fdcb329c80bb7448ab6f63 100644
--- a/frontend/src/store/mutations.ts
+++ b/frontend/src/store/mutations.ts
@@ -2,7 +2,7 @@ import Vue from 'vue'
 import { getStoreBuilder } from 'vuex-typex'
 
 import { initialState, RootState } from '@/store/store'
-import { Exam, Feedback, Statistics, StudentInfoForListView, SubmissionNoType, SubmissionType, Tutor } from '@/models'
+import { Exam, Statistics, StudentInfoForListView, SubmissionNoType, SubmissionType} from '@/models'
 
 export const mb = getStoreBuilder<RootState>()
 
diff --git a/functional_tests/test_auto_logout.py b/functional_tests/test_auto_logout.py
index ec268c7340359167e1de5f8403ac8ad675b599de..ded3555ba7da4782b33964509ea877450ad02d9d 100644
--- a/functional_tests/test_auto_logout.py
+++ b/functional_tests/test_auto_logout.py
@@ -7,7 +7,7 @@ from selenium.webdriver.support.ui import WebDriverWait
 from selenium.webdriver.support import expected_conditions as ec
 
 from core.models import UserAccount
-from functional_tests.util import (login, create_browser)
+from functional_tests.util import (login, create_browser, reset_browser_after_test)
 from util import factory_boys as fact
 
 log = logging.getLogger(__name__)
@@ -39,6 +39,9 @@ class TestAutoLogout(LiveServerTestCase):
             role=self.role
         )
 
+    def tearDown(self):
+        reset_browser_after_test(self.browser, self.live_server_url)
+
     def _login(self):
         login(self.browser, self.live_server_url, self.username, self.password)
 
diff --git a/functional_tests/test_export_modal.py b/functional_tests/test_export_modal.py
index 92d08bcc78500937d9b61cd70a9207fd86fdcac9..5f82e207628d20cbeb617483ddc3a276c5671f39 100644
--- a/functional_tests/test_export_modal.py
+++ b/functional_tests/test_export_modal.py
@@ -112,6 +112,7 @@ class ExportTestModal(LiveServerTestCase):
 
     def test_export_student_scores_as_json(self):
         fact.StudentInfoFactory()
+        fact.SubmissionFactory()
         self._login()
         export_btn = self.browser.find_element_by_id('export-btn')
         export_btn.click()
diff --git a/functional_tests/test_feedback_creation.py b/functional_tests/test_feedback_creation.py
index bc81158aac6bb666b9d3f46c9f17387752e75462..3c4ad16174c3a2c289b986355f6b6be2137ec290 100644
--- a/functional_tests/test_feedback_creation.py
+++ b/functional_tests/test_feedback_creation.py
@@ -9,7 +9,8 @@ from core.models import UserAccount, Submission, FeedbackComment
 from functional_tests.util import (login, create_browser, reset_browser_after_test,
                                    go_to_subscription, wait_until_code_changes,
                                    correct_some_submission,
-                                   reconstruct_submission_code, wait_until_element_count_equals)
+                                   reconstruct_submission_code, wait_until_element_count_equals,
+                                   reconstruct_solution_code)
 from util import factory_boys as fact
 
 
@@ -71,8 +72,8 @@ class UntestedParent:
                 f'{self.sub_type.name} - Full score: {self.sub_type.full_score}',
                 title.text
             )
-            solution = sub_type_el.find_element_by_class_name('solution-code')
-            self.assertEqual(self.sub_type.solution, solution.get_attribute('textContent'))
+            solution = reconstruct_solution_code(self)
+            self.assertEqual(self.sub_type.solution, solution)
             description = sub_type_el.find_element_by_class_name('type-description')
             html_el_in_desc = description.find_element_by_tag_name('h1')
             self.assertEqual('This', html_el_in_desc.text)
diff --git a/functional_tests/test_feedback_update.py b/functional_tests/test_feedback_update.py
index 30bf1a2d7c23ed2059015518cdb313fc5db1007b..a7fdbe9face6a7f771eeb98907dcc6f09d022b0f 100644
--- a/functional_tests/test_feedback_update.py
+++ b/functional_tests/test_feedback_update.py
@@ -5,7 +5,8 @@ from selenium.webdriver.support import expected_conditions as ec
 from selenium.webdriver.support.ui import WebDriverWait
 
 from functional_tests.util import (login, create_browser, go_to_subscription,
-                                   reconstruct_submission_code, correct_some_submission)
+                                   reconstruct_submission_code, correct_some_submission,
+                                   reset_browser_after_test)
 from util import factory_boys as fact
 
 
@@ -36,6 +37,9 @@ class TestFeedbackUpdate(LiveServerTestCase):
         self.sub_type = fact.SubmissionTypeFactory.create()
         fact.SubmissionFactory.create_batch(2, type=self.sub_type)
 
+    def tearDown(self):
+        reset_browser_after_test(self.browser, self.live_server_url)
+
     def _login(self):
         login(self.browser, self.live_server_url, self.username, self.password)
 
diff --git a/functional_tests/test_front_pages.py b/functional_tests/test_front_pages.py
index 742aacfdb6cda885ae225f6d26d320abf1216e03..a213fa006e73f121cc2f2fdd5a917a2f60d1916a 100644
--- a/functional_tests/test_front_pages.py
+++ b/functional_tests/test_front_pages.py
@@ -74,6 +74,9 @@ class FrontPageTestsTutor(UntestedParent.FrontPageTestsTutorReviewer):
             password=self.password
         )
 
+    def tearDown(self):
+        reset_browser_after_test(self.browser, self.live_server_url)
+
     def test_side_bar_contains_correct_items(self):
         self._login()
         drawer = self.browser.find_element_by_class_name('v-navigation-drawer')
@@ -101,6 +104,9 @@ class FrontPageTestsReviewer(UntestedParent.FrontPageTestsTutorReviewer):
             role=self.role
         )
 
+    def tearDown(self):
+        reset_browser_after_test(self.browser, self.live_server_url)
+
     def test_side_bar_contains_correct_items(self):
         self._login()
         drawer = self.browser.find_element_by_class_name('v-navigation-drawer')
diff --git a/functional_tests/test_solution_comments.py b/functional_tests/test_solution_comments.py
new file mode 100644
index 0000000000000000000000000000000000000000..59358ece759402918f42d5456881676d90d45cbf
--- /dev/null
+++ b/functional_tests/test_solution_comments.py
@@ -0,0 +1,142 @@
+from django.test import LiveServerTestCase
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+from selenium.webdriver.remote.webelement import WebElement
+from selenium.webdriver.support import expected_conditions as ec
+from selenium.webdriver.support.ui import WebDriverWait
+
+from core import models
+from functional_tests.util import (login, create_browser, query_returns_object,
+                                   reset_browser_after_test)
+from util import factory_boys as fact
+
+
+class TestSolutionComments(LiveServerTestCase):
+    browser: webdriver.Firefox = None
+    username = None
+    password = None
+    role = None
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.browser = create_browser()
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        cls.browser.quit()
+
+    def setUp(self):
+        super().setUp()
+        self.username = 'tut'
+        self.password = 'p'
+        fact.UserAccountFactory(
+            username=self.username,
+            password=self.password,
+        )
+        self.sub_type = fact.SubmissionTypeFactory.create()
+
+    def tearDown(self):
+        reset_browser_after_test(self.browser, self.live_server_url)
+
+    def _login(self):
+        login(self.browser, self.live_server_url, self.username, self.password)
+
+    def _write_comment(self, text="A comment", line_no=1):
+        sub_types = self.browser.find_element_by_id('submission-types-list')
+        sub_types.find_element_by_tag_name('a').click()
+        solution_table = self.browser.find_element_by_class_name('solution-table')
+        tr_of_line = solution_table.find_element_by_id(f'solution-line-{line_no}')
+        tr_of_line.find_element_by_class_name('line-number-btn').click()
+        comment_input = tr_of_line.find_element_by_name('solution-comment-input')
+        comment_input.send_keys(text)
+        solution_table.find_element_by_id('submit-comment').click()
+
+    def _edit_comment(self, old_text, new_text) -> WebElement:
+        solution_table = self.browser.find_element_by_class_name('solution-table')
+        comment = solution_table.find_element_by_xpath(
+            f"//div[@class='dialog-box'  and .//*[contains(text(), '{old_text}')]]"
+        )
+        comment.find_element_by_class_name('edit-button').click()
+        comment_input = solution_table.find_element_by_name('solution-comment-edit')
+        comment_input.send_keys(new_text)
+        solution_table.find_element_by_id('submit-comment').click()
+        return comment
+
+    def test_tutor_can_add_comment(self):
+        self._login()
+        comment_text = 'A comment!'
+        self._write_comment(comment_text, 1)
+        solution_table = self.browser.find_element_by_class_name('solution-table')
+        displayed_text = solution_table.find_element_by_class_name('message').text
+        self.assertEqual(comment_text, displayed_text)
+        comment_obj = models.SolutionComment.objects.first()
+        self.assertEqual(comment_text, comment_obj.text)
+        self.assertEqual(1, comment_obj.of_line)
+
+    def test_tutor_can_delete_own_comment(self):
+        self._login()
+        self._write_comment()
+        solution_table = self.browser.find_element_by_class_name('solution-table')
+        solution_table.find_element_by_class_name('delete-button').click()
+        self.browser.find_element_by_id('confirm-delete-comment').click()
+        WebDriverWait(self.browser, 10).until_not(
+            query_returns_object(models.SolutionComment),
+            "Solution comment not deleted."
+        )
+
+    def test_tutor_can_edit_own_comment(self):
+        self._login()
+        old_text = 'A comment'
+        new_text = 'A new text'
+        self._write_comment(old_text)
+        comment_obj = models.SolutionComment.objects.first()
+        self.assertEqual(old_text, comment_obj.text)
+        comment_el = self._edit_comment(old_text, new_text)
+        displayed_text = comment_el.find_element_by_class_name('message').text
+        self.assertEqual(new_text, displayed_text)
+        comment_obj.refresh_from_db()
+        self.assertEqual(new_text, comment_obj.text)
+
+    def test_tutor_can_not_delete_edit_other_comment(self):
+        self._login()
+        self._write_comment()
+        username = 'tut2'
+        password = 'p'
+        fact.UserAccountFactory(username=username, password=password)
+        reset_browser_after_test(self.browser, self.live_server_url)
+        login(self.browser, self.live_server_url, username, password)
+        sub_types = self.browser.find_element_by_id('submission-types-list')
+        sub_types.find_element_by_tag_name('a').click()
+        solution_table = self.browser.find_element_by_class_name('solution-table')
+        # Set the implicit wait for those to shorter, to reduce test run time
+        self.browser.implicitly_wait(2)
+        edit_buttons = solution_table.find_elements_by_class_name('edit-button')
+        delete_buttons = solution_table.find_elements_by_class_name('delete-button')
+        self.browser.implicitly_wait(10)
+        self.assertEqual(0, len(edit_buttons))
+        self.assertEqual(0, len(delete_buttons))
+
+    def test_reviewer_can_delete_tutor_comment(self):
+        self._login()
+        self._write_comment()
+        username = 'rev'
+        password = 'p'
+        fact.UserAccountFactory(
+            username=username, password=password, role=models.UserAccount.REVIEWER
+        )
+        reset_browser_after_test(self.browser, self.live_server_url)
+        login(self.browser, self.live_server_url, username, password)
+        sub_types = self.browser.find_element_by_id('submission-types-list')
+        sub_types.find_element_by_tag_name('a').click()
+        solution_table = self.browser.find_element_by_class_name('solution-table')
+        solution_table.find_element_by_class_name('delete-button').click()
+        self.browser.find_element_by_id('confirm-delete-comment').click()
+        WebDriverWait(self.browser, 10).until_not(
+            ec.presence_of_element_located((By.CLASS_NAME, 'dialog-box'))
+        )
+        WebDriverWait(self.browser, 10).until_not(
+            query_returns_object(models.SolutionComment),
+            "Solution comment not deleted."
+        )
diff --git a/functional_tests/util.py b/functional_tests/util.py
index 7d10f0dc703e88d1c695a76728265f4e1b9fb847..d35addf292241c5f970b8bd22aace4553cfcbd52 100644
--- a/functional_tests/util.py
+++ b/functional_tests/util.py
@@ -120,7 +120,16 @@ def correct_some_submission(test_class_instance):
 
 def reconstruct_submission_code(test_class_instance):
     sub_table = test_class_instance.browser.find_element_by_class_name('submission-table')
-    lines = sub_table.find_elements_by_tag_name('tr')
+    return reconstruct_code_from_table(sub_table)
+
+
+def reconstruct_solution_code(test_class_instance):
+    solution_table = test_class_instance.browser.find_element_by_class_name('solution-table')
+    return reconstruct_code_from_table(solution_table)
+
+
+def reconstruct_code_from_table(table_el):
+    lines = table_el.find_elements_by_tag_name('tr')
     line_no_code_pairs = [
         (line.get_attribute('id'),
             # call get_attribute here to get non normalized text