Gitlab Community Edition Instance

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • j.michal/grady
1 result
Show changes
Commits on Source (40)
Showing
with 449 additions and 160 deletions
......@@ -81,9 +81,9 @@ build_test_image:
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker pull $DEV_IMAGE_BASE || true
- docker build --cache-from $DEV_IMAGE_BASE -t $DEV_IMAGE_BASE --target node .
- docker build --network=host --cache-from $DEV_IMAGE_BASE -t $DEV_IMAGE_BASE --target node .
- docker pull $DEV_IMAGE || true
- docker build --cache-from $DEV_IMAGE --cache-from $DEV_IMAGE_BASE -t $DEV_IMAGE .
- docker build --network=host --cache-from $DEV_IMAGE --cache-from $DEV_IMAGE_BASE -t $DEV_IMAGE .
- docker push $DEV_IMAGE_BASE
- docker push $DEV_IMAGE
tags:
......
......@@ -21,6 +21,7 @@ class CorrectorSerializer(DynamicFieldsModelSerializer):
write_only=True,
required=False
)
role = serializers.CharField(read_only=True)
def get_feedback_created(self, t):
''' It is required that this field was previously annotated '''
......@@ -57,4 +58,6 @@ class CorrectorSerializer(DynamicFieldsModelSerializer):
'is_active',
'username',
'feedback_created',
'feedback_validated')
'feedback_validated',
'exercise_groups',
'role')
......@@ -93,10 +93,10 @@ class StudentPageTests(APITestCase):
self.assertEqual(3, len(self.rev_response.data))
@override_config(EXERCISE_MODE=True)
def test_tutor_can_only_see_students_when_in_exercise_mode(self):
def test_tutor_can_only_see_group_members_when_in_exercise_mode(self):
force_authenticate(self.request, user=self.tutor)
response = self.view(self.request)
self.assertEqual(3, len(response.data))
self.assertEqual(2, len(response.data))
def test_submissions_score_is_included(self):
res_with_sub = None
......
......@@ -94,3 +94,77 @@ class TutorReviewerCanChangePasswordTests(APITestCase):
self.client.force_authenticate(user=self.reviewer)
res = self.client.patch(url, data)
self.assertEqual(status.HTTP_403_FORBIDDEN, res.status_code)
class ReviewerCanChangeCorrectorRoleTests(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_factory = GradyUserFactory()
def setUp(self):
self.reviewer1 = self.user_factory.make_reviewer()
self.client = APIClient()
def _set_role(self, new_value, changing_user, user_to_change):
self.client.force_authenticate(user=changing_user)
url = f"/api/user/{user_to_change.pk}/change_role/"
return self.client.patch(url, data={'role': new_value})
def _make_reviewer(self, changing_user, user_to_change):
return self._set_role('Reviewer', changing_user, user_to_change)
def _make_tutor(self, changing_user, user_to_change):
return self._set_role('Tutor', changing_user, user_to_change)
def test_reviewer_can_promote_tutor_to_reviewer(self):
tutor = self.user_factory.make_tutor()
response = self._make_reviewer(self.reviewer1, tutor)
self.assertEqual(response.status_code, status.HTTP_200_OK)
tutor.refresh_from_db()
self.assertTrue(tutor.is_reviewer())
def test_reviewer_can_demote_other_reviewer_to_tutor(self):
reviewer2 = self.user_factory.make_reviewer()
response = self._make_tutor(self.reviewer1, reviewer2)
self.assertEqual(response.status_code, status.HTTP_200_OK)
reviewer2.refresh_from_db()
self.assertFalse(reviewer2.is_reviewer())
def test_reviewer_cannot_promote_student_to_reviewer(self):
exam = make_exams(exams=[{
'module_reference': 'Test Exam 01',
'total_score': 100,
'pass_score': 60,
}])[0]
student = self.user_factory.make_student(exam=exam)
response = self._make_reviewer(self.reviewer1, student)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_reviewer_cannot_promote_student_to_tutor(self):
exam = make_exams(exams=[{
'module_reference': 'Test Exam 01',
'total_score': 100,
'pass_score': 60,
}])[0]
student = self.user_factory.make_student(exam=exam)
response = self._make_tutor(self.reviewer1, student)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_student_cannot_change_access_rights(self):
exam = make_exams(exams=[{
'module_reference': 'Test Exam 01',
'total_score': 100,
'pass_score': 60,
}])[0]
student = self.user_factory.make_student(exam=exam)
response = self._make_reviewer(student, self.reviewer1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_tutor_cannot_change_access_rights(self):
tutor = self.user_factory.make_tutor()
response = self._make_reviewer(tutor, self.reviewer1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_reviewer_cannot_demote_self_to_tutor(self):
response = self._make_tutor(self.reviewer1, self.reviewer1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
......@@ -81,7 +81,9 @@ class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet):
return queryset
elif self.request.user.is_tutor() and config.EXERCISE_MODE:
return queryset
return queryset.filter(
user__exercise_groups__in=self.request.user.exercise_groups.all()
)
else:
return []
......@@ -328,6 +330,57 @@ class UserAccountViewSet(viewsets.ReadOnlyModelViewSet):
user.save()
return Response(status.HTTP_200_OK)
@action(detail=True, methods=['patch'], permission_classes=(IsReviewer,))
def change_groups(self, request, *args, **kwargs):
# for some reason only the newly added groups come as a group object
groups = [x.get('pk') if type(x) is not str else x for x in request.data]
req_user = request.user
user = self.get_object()
if groups is None:
error_msg = "You need to provide an 'groups' field"
return Response({'Error': error_msg}, status.HTTP_400_BAD_REQUEST)
if req_user.is_student() or req_user.is_tutor():
return Response(status.HTTP_403_FORBIDDEN)
user.set_groups(groups)
user.save()
return Response(status.HTTP_200_OK)
@action(detail=True)
def get_groups(self, request, *args, **kwargs):
req_user = request.user
if req_user.is_student() or req_user.is_tutor():
return Response(status.HTTP_403_FORBIDDEN)
user = self.get_object()
return Response(user.exercise_groups, status=status.HTTP_200_OK)
@action(detail=True, methods=["patch"])
def change_role(self, request, *args, **kwargs):
new_role = request.data.get('role')
user = self.get_object()
valid_values = [
models.UserAccount.STUDENT,
models.UserAccount.REVIEWER,
models.UserAccount.TUTOR,
]
if new_role not in valid_values:
error_msg = (
"You need to provide a 'role' field with one of these values: "
+ ', '.join(valid_values)
)
return Response({'Error': error_msg}, status.HTTP_400_BAD_REQUEST)
if not request.user.is_reviewer():
error_msg = 'Only reviewers can manage access rights.'
return Response({'Error': error_msg}, status.HTTP_403_FORBIDDEN)
if user.is_student():
error_msg = 'Cannot promote a student to another role.'
return Response({'Error': error_msg}, status.HTTP_403_FORBIDDEN)
if user == request.user and not new_role == models.UserAccount.REVIEWER:
error_msg = 'As a reviewer, you cannot demote yourself.'
return Response({'Error': error_msg}, status.HTTP_403_FORBIDDEN)
user.role = new_role
user.save()
return Response(status.HTTP_200_OK)
@action(detail=False)
def me(self, request):
serializer = self.get_serializer(request.user)
......
......@@ -153,6 +153,15 @@ export async function fetchGroups(): Promise<Group[]> {
return (await ax.get(url)).data
}
export async function fetchUserGroups(userPk: string): Promise<Group[]> {
const url = `/api/user/${userPk}/get_groups/`
return (await ax.get(url)).data
}
export async function setGroups (userPk: string, groups: Group[]): Promise<UserAccount> {
return (await ax.patch(`/api/user/${userPk}/change_groups/`, groups)).data
}
export async function deleteSolutionComment (pk: number): Promise<AxiosResponse<void>> {
const url = `/api/solution-comment/${pk}/`
return ax.delete(url)
......@@ -213,10 +222,18 @@ export async function changeActiveForUser (userPk: string, active: boolean): Pro
return (await ax.patch(`/api/user/${userPk}/change_active/`, { 'is_active': active })).data
}
export async function changeUserRole (userPk: string, role: UserAccount.RoleEnum): Promise<UserAccount> {
return (await ax.patch(`/api/user/${userPk}/change_role/`, { role })).data
}
export async function fetchUsers (): Promise<UserAccount[]> {
return (await ax.get('api/user/')).data
}
export async function fetchUser(userPk: string): Promise<UserAccount> {
return (await ax.get(`/api/user/${userPk}`)).data
}
export async function getLabels (): Promise<FeedbackLabel[]> {
return (await ax.get('/api/label/')).data
}
......
......@@ -37,23 +37,6 @@
</v-toolbar>
<slot name="sidebar-content" />
<div class="sidebar-footer">
<v-tooltip
top
style="min-width: 150px"
>
<template #activator="{ on }">
<div v-on="on">
<v-switch
v-model="$vuetify.theme.dark"
class="dark-mode-switch"
:disabled="!darkModeUnlocked"
:label="mini ? '' : 'dark mode'"
/>
</div>
</template>
<span v-if="darkModeUnlocked">Experimental: styling issues may occur!</span>
<span v-else>You need to visit the feedback site below first!</span>
</v-tooltip>
<v-btn
id="feedback-btn"
href="https://gitlab.gwdg.de/j.michal/grady/issues"
......@@ -63,8 +46,7 @@
:text="mini"
:tile="!mini"
depressed
:class="{ 'fab-button': mini, 'fab-button-white': !darkMode }"
@click.native="logFeedbackClick"
:class="{ 'fab-button': mini }"
>
<v-icon :left="!mini">
feedback
......@@ -112,10 +94,6 @@ export default {
...mapStateToComputedGetterSetter({
pathPrefix: 'UI',
items: [
{
name: 'darkModeUnlocked',
mutation: UI.SET_DARK_MODE_UNLOCKED
},
{
name: 'mini',
path: 'sideBarCollapsed',
......@@ -124,11 +102,6 @@ export default {
]
})
},
methods: {
logFeedbackClick () {
this.darkModeUnlocked = true
}
}
}
</script>
......@@ -154,10 +127,6 @@ export default {
.fab-button-white {
color: grey !important;
}
.dark-mode-switch {
margin-left: 22px;
}
</style>
<style>
......
......@@ -124,16 +124,16 @@ export default {
})
this.$emit('hide')
}).catch(error => {
if (error.response && error.response.status === 401) {
this.oldPasswordRejected = true
if (error.response && error.response.status === 401) {
this.oldPasswordRejected = true
this.$refs.form.validate()
return
}
}
if (error.response && error.response.status === 406) {
this.newPasswordErrors = error.response.data.new_password
if (error.response && error.response.data && error.response.data.newPassword) {
this.newPasswordErrors = error.response.data.newPassword
return
}
}
this.errorAlert = error.toString()
})
......
......@@ -55,6 +55,8 @@
required
:rules="[ required ]"
type="password"
:error-messages="passwordErrors"
@input="passwordErrors = null"
/>
<v-text-field
id="input-register-password-repeat"
......@@ -105,10 +107,11 @@ export default {
loading: false,
acceptedGDPR: false,
registrationFormIsValid: false,
required: required,
required,
checkPasswordsMatch: v => v === this.credentials.password || 'Passwords do not match.',
errorAlert: null,
usernameErrors: null,
passwordErrors: null,
}
},
watch: {
......@@ -129,13 +132,13 @@ export default {
registerTutor(this.credentials).then(() => {
this.$emit('registered', this.credentials)
}).catch(error => {
if (error.response && error.response.data && error.response.data.username)
this.usernameErrors = error.response.data.username
else
this.usernameErrors = error.response && error.response.data && error.response.data.username
this.passwordErrors = error.response && error.response.data && error.response.data.password
if (!this.usernameErrors && !this.passwordErrors)
this.errorAlert = `Couldn't register a tutor account: ${error}`
}).finally(() => { this.loading = false })
}
}
},
}
</script>
......
<template>
<v-row
class="pane-wrapper"
no-gutters
>
<v-col class="pane">
<slot name="left" />
</v-col>
<v-divider
v-if="showRightPane"
vertical
/>
<v-col
v-if="showRightPane"
class="pane"
>
<slot name="right" />
</v-col>
</v-row>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class TwoPaneLayout extends Vue {
@Prop({ type: Boolean, default: true }) showRightPane!: boolean
}
</script>
<style scoped>
.pane-wrapper {
height: 100%;
}
.pane {
height: 100%;
overflow: auto;
}
</style>
......@@ -24,6 +24,16 @@
>
Change password
</v-list-item>
<v-list-item @click.capture.stop="$vuetify.theme.dark = !$vuetify.theme.dark">
<v-list-item-content>
<v-list-item-title>
Dark mode (experimental)
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-switch v-model="$vuetify.theme.dark" />
</v-list-item-action>
</v-list-item>
<v-divider class="my-2" />
<v-list-item @click="logout">
<v-icon left>
......
......@@ -2,35 +2,28 @@
<v-card>
<v-card-title>Assign labels</v-card-title>
<v-divider />
<v-row>
<v-col
class="ml-2"
sm="10"
<v-card-text>
<v-autocomplete
id="label-add-autocomplete"
:items="feedbackLabels"
item-text="name"
item-value="pk"
append-icon="search"
placeholder="search for keywords"
@keyup.enter.ctrl.exact="submitFeedback"
@input="addLabel"
>
<v-autocomplete
id="label-add-autocomplete"
:items="feedbackLabels"
item-text="name"
item-value="pk"
append-icon="search"
placeholder="search for keywords"
@keyup.enter.ctrl.exact="submitFeedback"
@input="addLabel"
>
<template #item="{ item }">
<div class="label-adder-item">
<feedback-label v-bind="item" />
</div>
</template>
</v-autocomplete>
</v-col>
<v-row
class="ml-2 mb-3"
>
<v-col sm="4">
<v-col sm="12">
<template #item="{ item }">
<div class="label-adder-item">
<feedback-label v-bind="item" />
</div>
</template>
</v-autocomplete>
<v-row>
<v-col md="4">
<div>
CURRENT LABELS
</v-col>
</div>
<feedback-label
v-for="label in unchangedMapped"
:key="label.pk"
......@@ -39,10 +32,10 @@
@remove-clicked="removeLabel"
/>
</v-col>
<v-col sm="4">
<v-col sm="12">
<v-col md="4">
<div>
WILL BE REMOVED
</v-col>
</div>
<feedback-label
v-for="label in removedMapped"
:key="label.pk"
......@@ -51,10 +44,10 @@
@remove-clicked="addLabel"
/>
</v-col>
<v-col sm="4">
<v-col sm="12">
<v-col md="4">
<div>
WILL BE ADDED
</v-col>
</div>
<feedback-label
v-for="label in addedMapped"
:key="label.pk"
......@@ -64,7 +57,7 @@
/>
</v-col>
</v-row>
</v-row>
</v-card-text>
</v-card>
</template>
......
......@@ -131,6 +131,7 @@ export default class FeedbackTable extends Vue {
queryFoundInFields(f: Feedback): boolean {
return f.ofSubmissionType !== undefined && this.queryFoundInString(f.ofSubmissionType) ||
this.exerciseMode && f.ofStudent !== undefined && this.queryFoundInString(f.ofStudent) ||
f.created !== undefined && this.queryFoundInString(f.created) ||
f.modified !== undefined && this.queryFoundInString(f.modified)
}
......
......@@ -228,15 +228,15 @@ export default {
return []
},
groups () {
return Assignments.state.groups.slice().sort((a, b) => {
const matches_a = a.name.match(/(\d+)/)
const number_a = Number(matches_a === null ? 0 : matches_a[1])
const matches_b = b.name.match(/(\d+)/)
const number_b = Number(matches_b === null ? 0 : matches_b[1])
return (number_a<number_b?-1:(number_a>number_b?1:0))
})
if (Authentication.isTutor) {
return Authentication.state.user.exerciseGroups
}
else if (Authentication.isReviewer) {
return Assignments.state.groups
}
else {
return []
}
},
},
created () {
......
<template>
<v-select
v-model="value"
:items="roleOptions"
filled
dense
hide-details
:loading="loading"
:disabled="isForSelf"
@change="updateRole"
/>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Prop, Watch } from 'vue-property-decorator'
import { Tutor, UserAccount } from '@/models'
import { changeUserRole } from '@/api'
import { Authentication } from '@/store/modules/authentication'
@Component
export default class RoleSelect extends Vue {
@Prop({ type: Object, required: true }) readonly tutor!: Tutor
roleOptions = [UserAccount.RoleEnum.Reviewer, UserAccount.RoleEnum.Tutor]
value = this.tutor.role
previousValue = this.value
loading = false
get isForSelf() {
return Authentication.state.user.pk === this.tutor.pk
}
async updateRole(newRole: UserAccount.RoleEnum) {
this.loading = true
try {
await changeUserRole(this.tutor.pk, newRole)
this.previousValue = newRole
} catch (error) {
this.value = this.previousValue
} finally {
this.loading = false
}
}
}
</script>
......@@ -34,6 +34,23 @@
<span>Free locked submissions</span>
</v-tooltip>
</template>
<template #item.exerciseGroups="{ item }">
<v-select
v-model="item.exerciseGroups"
item-text="name"
item-value="pk"
:items="groups"
label="Set Groups"
single-line
return-object
multiple
chips
dense
hide-details
filled
@change="setExerciseGroups($event, item)"
/>
</template>
<template #item.isActive="{ item }">
<v-btn
v-if="canRevokeAccess(item.username)"
......@@ -62,6 +79,9 @@
</v-tooltip>
</v-btn>
</template>
<template #item.role="{ item }">
<role-select :tutor="item" />
</template>
</v-data-table>
</v-card>
</template>
......@@ -69,13 +89,15 @@
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { changeActiveForUser } from '@/api'
import { changeActiveForUser, setGroups, fetchUserGroups, fetchUser } from '@/api'
import { actions } from '@/store/actions'
import { Authentication } from '@/store/modules/authentication'
import { TutorOverview } from '@/store/modules/tutor-overview'
import { Tutor } from '@/models'
import { Group, Tutor, UserAccount } from '@/models'
import { Assignments } from '@/store/modules/assignments'
import RoleSelect from './RoleSelect.vue'
@Component
@Component({ components: { RoleSelect } })
export default class TutorList extends Vue {
headers = [
{
......@@ -98,21 +120,60 @@ export default class TutorList extends Vue {
align: 'right',
value: 'reservedSubmissions'
},
{
text: 'Exercise Groups',
align: 'right',
value: 'exerciseGroups'
},
{
text: 'Has Access',
align: 'right',
value: 'isActive'
},
{
text: 'Role',
value: 'role'
}
]
get tutors () {
return TutorOverview.state.tutors.map(tutor => {
var tlist = TutorOverview.state.tutors.map(tutor => {
var groups: Group[] = []
this.userAccountGroups(tutor).then(function(value) {
groups = value // Success!
}, (reason) => {
this.$notify({
title: 'Error',
text: `Unable to fetch tutors: ${reason}`,
type: 'error'
})
return []
})
const reservedSubmissions = TutorOverview.state.activeAssignments[tutor.pk]
return {
...tutor,
reservedSubmissions: reservedSubmissions ? reservedSubmissions.length : 0
reservedSubmissions: reservedSubmissions ? reservedSubmissions.length : 0,
}
})
return tlist
}
get groups () {
return Assignments.state.groups.slice().sort((a, b) => {
const matches_a = a.name.match(/(\d+)/)
const number_a = Number(matches_a === null ? 0 : matches_a[1])
const matches_b = b.name.match(/(\d+)/)
const number_b = Number(matches_b === null ? 0 : matches_b[1])
return (number_a<number_b?-1:(number_a>number_b?1:0))
})
}
async userAccountGroups(tutor: Tutor) {
const groups = await (await fetchUser(tutor.pk)).exerciseGroups
return groups
}
changeActiveStatus (tutor: Tutor) {
......@@ -127,6 +188,18 @@ export default class TutorList extends Vue {
})
}
setExerciseGroups (groups: Group[], tutor: Tutor){
setGroups(tutor.pk, groups).then(() => {
TutorOverview.getTutors()
}).catch(() => {
this.$notify({
title: 'Error',
text: `Unable to change exercise-groups of ${tutor.username}`,
type: 'error'
})
})
}
deleteAssignmentsOfTutor (tutor: Tutor) {
TutorOverview.deleteActiveAssignmentsOfTutor(tutor)
}
......
......@@ -783,6 +783,18 @@ export interface Tutor {
* @memberof Tutor
*/
feedbackValidated?: string
/**
*
* @type {Group}
* @memberof Tutor
*/
exerciseGroups: Group[]
/**
* @type {string}
* @memberof Tutor
*/
role: UserAccount.RoleEnum
}
/**
......
<template>
<div>
<component :is="layout" />
<v-main>
<v-main class="main-content">
<router-view />
</v-main>
</div>
......@@ -39,6 +39,20 @@ export default {
}
</script>
<style scoped>
<style>
html {
/* Vuetify always shows the scrollbar by default. This disables it. */
overflow-y: auto !important;
}
</style>
<style scoped>
/* Move the scrollbar below the header so it doesn't jump around when no scrollbar is shown. */
.main-content {
/* 48px is the vuetify header size. */
height: calc(100vh - 48px);
margin-top: 48px;
padding-top: 0 !important;
overflow: auto;
}
</style>
<template>
<v-container>
<v-row>
<route-change-confirmation :next-route="nextRoute" />
<v-col
:cols="showSubmissionType ? 6 : 12"
>
<two-pane-layout :show-right-pane="showSubmissionType">
<template #left>
<v-container>
<route-change-confirmation :next-route="nextRoute" />
<submission-correction
:key="currentAssignment.pk"
:assignment="currentAssignment"
......@@ -14,27 +12,20 @@
:tests="submission.tests"
:expand="true"
/>
</v-col>
<v-col
v-if="showSubmissionType"
cols="6"
>
<div class="sub-correction">
<submission-type
:key="submissionType.pk"
v-bind="submissionType"
:reverse="true"
:expanded-by-default="{ Description: true, Solution: true }"
/>
</div>
</v-col>
</v-row>
</v-container>
</v-container>
</template>
<template #right>
<submission-type
:key="submissionType.pk"
v-bind="submissionType"
:expanded-by-default="{ Description: true, Solution: true }"
/>
</template>
</two-pane-layout>
</template>
<script lang="ts">
import { Vue, Component} from 'vue-property-decorator'
import { Vue, Component, Watch } from 'vue-property-decorator'
import { Route, NavigationGuard } from 'vue-router'
import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection.vue'
import SubmissionType from '@/components/submission_type/SubmissionType.vue'
......@@ -46,6 +37,7 @@ import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeCo
import { getters } from '@/store/getters'
import { SubmissionAssignment } from '@/models'
import { UI } from '@/store/modules/ui'
import TwoPaneLayout from '@/components/TwoPaneLayout.vue'
const onRouteEnterOrUpdate: NavigationGuard = function (to, from, next) {
Assignments.changeAssignment(to).then(() => {
......@@ -61,7 +53,8 @@ const onRouteEnterOrUpdate: NavigationGuard = function (to, from, next) {
RouteChangeConfirmation,
SubmissionTests,
SubmissionType,
SubmissionCorrection
SubmissionCorrection,
TwoPaneLayout,
}
})
export default class SubscriptionWorkPage extends Vue {
......@@ -116,14 +109,10 @@ export default class SubscriptionWorkPage extends Vue {
this.$router.replace({name: 'correction-ended'})
})
}
}
</script>
<style scoped>
.sub-correction {
position: sticky;
top: 64px;
overflow-y: auto;
height: 90vh;
@Watch('currentAssignment')
onCurrentAssignmentChanged() {
window.scrollTo(0, 0)
}
</style>
}
</script>
<template>
<v-container>
<v-row>
<v-col cols="6">
<router-view name="left" />
</v-col>
<v-col
cols="6"
>
<div class="right-view">
<router-view
name="right"
@refresh="refresh"
/>
</div>
</v-col>
</v-row>
</v-container>
<two-pane-layout>
<template #left>
<router-view name="left" />
</template>
<template #right>
<v-container>
<router-view
name="right"
@refresh="refresh"
/>
</v-container>
</template>
</two-pane-layout>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'
import TwoPaneLayout from '@/components/TwoPaneLayout.vue'
@Component
@Component({
components: { TwoPaneLayout }
})
export default class FeedbackHistoryPage extends Vue {
refresh () {
FeedbackTable.getFeedbackHistory()
......@@ -34,12 +33,3 @@ export default class FeedbackHistoryPage extends Vue {
}
}
</script>
<style scoped>
.right-view {
position: sticky;
top: 80px;
overflow-y: scroll;
height: 90vh;
}
</style>