From 67470b54b2041475f74c7c50e89fbf8e49e7a2da Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Fri, 22 Dec 2017 22:02:18 +0100 Subject: [PATCH] Tutor Layout and SubmissioCorrectionPage Basic TutorLayout and SubmissionCorrectionPage. Added new dependency v-clipboard. Run yarn install to update. --- core/serializers.py | 11 ++- frontend/package.json | 1 + frontend/src/components/SubmissionType.vue | 34 ++++++-- .../src/components/student/SubmissionList.vue | 2 +- .../submission_notes/AnnotatedSubmission.vue | 64 ++++++++++----- .../submission_notes/CorrectionHelpCard.vue | 38 +++++++++ .../submission_notes/FeedbackComment.vue | 2 +- .../submission_notes/FeedbackForm.vue | 19 +++-- .../AnnotatedSubmissionBottomToolbar.vue | 64 +++++++++++++++ .../AnnotatedSubmissionTopToolbar.vue | 44 ++++++++++ frontend/src/main.js | 2 + frontend/src/pages/Login.vue | 17 ++-- .../src/pages/SubmissionCorrectionPage.vue | 81 +++++++++++++++++++ frontend/src/pages/student/StudentLayout.vue | 1 + frontend/src/pages/tutor/TutorLayout.vue | 57 +++++++++++++ frontend/src/router/index.js | 15 +++- frontend/src/store/store.js | 14 +++- frontend/yarn.lock | 4 + 18 files changed, 423 insertions(+), 47 deletions(-) create mode 100644 frontend/src/components/submission_notes/CorrectionHelpCard.vue create mode 100644 frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue create mode 100644 frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue create mode 100644 frontend/src/pages/SubmissionCorrectionPage.vue create mode 100644 frontend/src/pages/tutor/TutorLayout.vue diff --git a/core/serializers.py b/core/serializers.py index 74db00cf..b64a4cf1 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -50,9 +50,16 @@ class TestSerializer(DynamicFieldsModelSerializer): fields = ('name', 'label', 'annotation') -class SubmissionTypeSerializer(DynamicFieldsModelSerializer): +class SubmissionTypeListSerializer(DynamicFieldsModelSerializer): fullScore = serializers.IntegerField(source='full_score') + class Meta: + model = SubmissionType + fields = ('id', 'name', 'fullScore') + + +class SubmissionTypeSerializer(SubmissionTypeListSerializer): + class Meta: model = SubmissionType fields = ('id', 'name', 'fullScore', 'description', 'solution') @@ -69,7 +76,7 @@ class SubmissionSerializer(DynamicFieldsModelSerializer): class SubmissionListSerializer(DynamicFieldsModelSerializer): - type = SubmissionTypeSerializer(fields=('id', 'name', 'fullScore')) + type = SubmissionTypeListSerializer() # TODO change this according to new feedback model feedback = FeedbackSerializer(fields=('score',)) diff --git a/frontend/package.json b/frontend/package.json index 9f4245d5..03ef1b1c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "axios": "^0.17.0", "google-code-prettify": "^1.0.5", "material-design-icons": "^3.0.1", + "v-clipboard": "^1.0.4", "vue": "^2.5.2", "vue-router": "^3.0.1", "vuetify": "^0.17.3", diff --git a/frontend/src/components/SubmissionType.vue b/frontend/src/components/SubmissionType.vue index 5827cbb6..6cf35fce 100644 --- a/frontend/src/components/SubmissionType.vue +++ b/frontend/src/components/SubmissionType.vue @@ -3,13 +3,13 @@ <h2 class="mb-2">{{ name }} - Full score: {{ fullScore }}</h2> <v-expansion-panel expand> <v-expansion-panel-content - v-for="(item, key, i) in {Description: description, Solution:solution}" + v-for="(item, i) in typeItems" :key="i" - :value="expandedByDefault[key]"> - <div slot="header">{{ key }}</div> + :value="expandedByDefault[item.title]"> + <div slot="header">{{ item.title }}</div> <v-card color="grey lighten-4"> <v-card-text> - {{ item }} + {{ item.text }} </v-card-text> </v-card> </v-expansion-panel-content> @@ -38,6 +38,10 @@ type: Number, required: true }, + reverse: { + type: Boolean, + default: false + }, expandedByDefault: { type: Object, default: function () { @@ -45,8 +49,26 @@ Description: true, Solution: true } - }, - required: false + } + } + }, + computed: { + typeItems () { + let items = [ + { + title: 'Description', + text: this.description + }, + { + title: 'Solution', + text: this.solution + } + ] + if (this.reverse) { + return items.reverse() + } else { + return items + } } } } diff --git a/frontend/src/components/student/SubmissionList.vue b/frontend/src/components/student/SubmissionList.vue index 7ef34f95..451404f9 100644 --- a/frontend/src/components/student/SubmissionList.vue +++ b/frontend/src/components/student/SubmissionList.vue @@ -10,7 +10,7 @@ <td>{{ props.item.type.name }}</td> <td class="text-xs-right">{{ props.item.feedback.score }}</td> <td class="text-xs-right">{{ props.item.type.fullScore }}</td> - <td class="text-xs-right"><v-btn :to="`submission/${props.item.type.id}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td> + <td class="text-xs-right"><v-btn :to="`/student/submission/${props.item.type.id}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td> </template> </v-data-table> <v-alert color="info" value="true"> diff --git a/frontend/src/components/submission_notes/AnnotatedSubmission.vue b/frontend/src/components/submission_notes/AnnotatedSubmission.vue index d5c24b1b..a3fdb504 100644 --- a/frontend/src/components/submission_notes/AnnotatedSubmission.vue +++ b/frontend/src/components/submission_notes/AnnotatedSubmission.vue @@ -1,33 +1,49 @@ <template> - <table class="elevation-1"> - <tr v-for="(code, index) in submission" :key="index"> - <td class="line-number-cell"> - <v-btn block class="line-number-btn" @click="toggleEditorOnLine(index)">{{ index }}</v-btn> - </td> - <td> - <pre class="prettyprint"><code class="lang-c"> {{ code }}</code></pre> - <feedback-comment - v-if="feedback[index] && !showEditorOnLine[index]" - @click="toggleEditorOnLine(index)">{{ feedback[index] }} - </feedback-comment> - <comment-form - v-if="showEditorOnLine[index] && editable" - @collapseFeedbackForm="showEditorOnLine[index] = false" - :feedback="feedback[index]" - :index="index"> - </comment-form> - </td> - </tr> - </table> + <v-container> + <annotated-submission-top-toolbar + v-if="isTutor || isReviewer" + class="mb-1 elevation-1" + :submission="rawSubmission" + /> + <table class="elevation-1"> + <tr v-for="(code, index) in submission" :key="index"> + <td class="line-number-cell"> + <v-btn block class="line-number-btn" @click="toggleEditorOnLine(index)">{{ index }}</v-btn> + </td> + <td> + <pre class="prettyprint"><code class="lang-c"> {{ code }}</code></pre> + <feedback-comment + v-if="feedback[index] && !showEditorOnLine[index]" + @click.native="toggleEditorOnLine(index)">{{ feedback[index] }} + </feedback-comment> + <comment-form + v-if="showEditorOnLine[index] && editable" + @collapseFeedbackForm="showEditorOnLine[index] = false" + :feedback="feedback[index]" + :index="index"> + </comment-form> + </td> + </tr> + </table> + <annotated-submission-bottom-toolbar + v-if="isTutor || isReviewer" + class="mt-1 elevation-1" + /> + </v-container> </template> <script> + import { mapGetters } from 'vuex' import CommentForm from '@/components/submission_notes/FeedbackForm.vue' import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue' + import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar' + import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar' export default { components: { + AnnotatedSubmissionBottomToolbar, + AnnotatedSubmissionTopToolbar, FeedbackComment, CommentForm}, name: 'annotated-submission', @@ -60,7 +76,12 @@ acc[index + 1] = cur return acc }, {}) - } + }, + ...mapGetters([ + 'isStudent', + 'isTutor', + 'isReviewer' + ]) }, methods: { toggleEditorOnLine (lineIndex) { @@ -79,6 +100,7 @@ table { table-layout: auto; border-collapse: collapse; + width: 100%; } diff --git a/frontend/src/components/submission_notes/CorrectionHelpCard.vue b/frontend/src/components/submission_notes/CorrectionHelpCard.vue new file mode 100644 index 00000000..ae204d18 --- /dev/null +++ b/frontend/src/components/submission_notes/CorrectionHelpCard.vue @@ -0,0 +1,38 @@ +<template> + <v-card class="help-card"> + <v-card-title> + <v-icon>help_outline</v-icon> + <h3>Tips on using the correction interface</h3> + </v-card-title> + <v-card-text> + Never trade an ale. + The sea-dog leads with yellow fever, crush the captain's quarters until it waves.<br> + Ho-ho-ho! malaria of life.<br> + Halitosis, adventure, and yellow fever.<br> + The girl drinks with halitosis, pull the galley before it laughs.<br> + The moon fires with life, vandalize the bikini atoll before it travels.<br> + The tuna blows with fight, haul the freighter before it whines.<br> + The cannibal robs with hunger, fire the lighthouse until it whines.<br> + The captain loves with death, vandalize the lighthouse before it whines.<br> + The anchor loots with treasure, raid the freighter before it grows.<br> + The reef commands with endurance, view the quarter-deck until it whines.<br> + The scallywag loots with passion, crush the bikini atoll before it falls.<br> + The sea leads with treasure, ransack the brig until it dies.<br> + The parrot robs with desolation, view the seychelles before it screams.<br> + The warm anchor quirky blows the landlubber.<br> + + </v-card-text> + </v-card> +</template> + +<script> + export default { + name: 'correction-help-card' + } +</script> + +<style scoped> + .help-card { + width: fit-content; + } +</style> diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue index 637fce73..8af24d6e 100644 --- a/frontend/src/components/submission_notes/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/FeedbackComment.vue @@ -27,7 +27,7 @@ } .tip-up { - top: -25px; /* Same as body margin top + border */ + top: -22px; /* Same as body margin top + border */ left: 10px; border-right-color: transparent; border-left-color: transparent; diff --git a/frontend/src/components/submission_notes/FeedbackForm.vue b/frontend/src/components/submission_notes/FeedbackForm.vue index a9937662..403d0015 100644 --- a/frontend/src/components/submission_notes/FeedbackForm.vue +++ b/frontend/src/components/submission_notes/FeedbackForm.vue @@ -3,15 +3,16 @@ <v-text-field name="feedback-input" label="Please provide your feedback here" - v-model="current_feedback" + v-model="currentFeedback" @keyup.enter.ctrl.exact="submitFeedback" @keyup.esc="collapseTextField" + @focus="selectInput($event)" rows="2" textarea autofocus auto-grow hide-details - ></v-text-field> + /> <v-btn color="success" @click="submitFeedback">Submit</v-btn> <v-btn @click="discardFeedback">Discard changes</v-btn> </div> @@ -23,27 +24,31 @@ name: 'comment-form', props: { feedback: String, - index: Number + index: String }, data () { return { - current_feedback: this.feedback + currentFeedback: this.feedback } }, methods: { - + selectInput (event) { + if (event) { + event.target.select() + } + }, collapseTextField () { this.$emit('collapseFeedbackForm') }, submitFeedback () { this.$store.dispatch('updateFeedback', { lineIndex: this.index, - content: this.current_feedback + content: this.currentFeedback }) this.collapseTextField() }, discardFeedback () { - this.current_feedback = this.feedback + this.currentFeedback = this.feedback } } } diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue new file mode 100644 index 00000000..887661e2 --- /dev/null +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue @@ -0,0 +1,64 @@ +<template> + <v-toolbar dense class="bottom-toolbar"> + <v-spacer/> + <v-alert + class="score-alert ma-3" + color="error" + icon="warning" + :value="scoreError" + >{{ scoreError }}</v-alert> + <span class="mr-2">Score:</span> + <input + class="score-text-field" + type="number" + v-model="score" + @input="validateScore" + @change="validateScore" + /> + <v-tooltip top> + <v-btn color="success" slot="activator">Submit<v-icon>chevron_right</v-icon></v-btn> + <span>Submit and continue</span> + </v-tooltip> + </v-toolbar> +</template> + +<script> + export default { + name: 'annotated-submission-bottom-toolbar', + data () { + return { + score: 42, + mockMax: 50, + scoreError: '' + + } + }, + methods: { + validateScore () { + if (this.score < 0) { + this.score = 0 + this.scoreError = 'Score must be 0 or greater.' + } else if (this.score > this.mockMax) { + this.score = this.mockMax + this.scoreError = `Score must be less or equal to ${this.mockMax}` + } + } + } + } +</script> + +<style scoped> + .bottom-toolbar { + font-size: large; + } + .score-text-field { + max-width: 50px; + box-sizing: border-box; + border: 1px solid grey; + border-radius: 2px; + padding: 3px; + } + .score-alert { + max-height: 40px; + } +</style> diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue new file mode 100644 index 00000000..845c6608 --- /dev/null +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue @@ -0,0 +1,44 @@ +<template> + <v-toolbar + dense> + <v-toolbar-side-icon @click.stop="helpDialog=true"> + <v-icon>help_outline</v-icon> + </v-toolbar-side-icon> + <v-dialog + scrollable + max-width="fit-content" + v-model="helpDialog" + > + <correction-help-card></correction-help-card> + </v-dialog> + <v-spacer></v-spacer> + <v-tooltip top> + <v-btn icon slot="activator" v-clipboard="submission"><v-icon>content_copy</v-icon></v-btn> + <span>Copy to clipboard</span> + </v-tooltip> + </v-toolbar> +</template> + +<script> + import CorrectionHelpCard from '@/components/submission_notes/CorrectionHelpCard' + + export default { + components: {CorrectionHelpCard}, + name: 'annotated-submission-top-toolbar', + props: { + submission: { + type: String, + required: true + } + }, + data () { + return { + helpDialog: false + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/main.js b/frontend/src/main.js index d7cf28b7..a92f0660 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -5,6 +5,7 @@ import App from './App' import router from './router' import store from './store/store' import Vuetify from 'vuetify' +import Cliboard from 'v-clipboard' import 'vuetify/dist/vuetify.min.css' import 'material-design-icons/iconfont/material-icons.css' @@ -12,6 +13,7 @@ import 'google-code-prettify/bin/prettify.min' import 'google-code-prettify/bin/prettify.min.css' Vue.use(Vuetify) +Vue.use(Cliboard) Vue.config.productionTip = false diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 8822993b..a5b5d1b4 100644 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -19,13 +19,13 @@ v-model="credentials.username" required autofocus - ></v-text-field> + /> <v-text-field label="Password" v-model="credentials.password" type="password" required - ></v-text-field> + /> <v-btn :loading="loading" type="submit" color="primary">Access</v-btn> </v-form> </v-flex> @@ -49,7 +49,8 @@ }, computed: { ...mapState([ - 'error' + 'error', + 'userRole' ]) }, methods: { @@ -62,10 +63,16 @@ submit () { this.loading = true this.getJWTToken(this.credentials).then(() => { - this.getUserRole() + this.getUserRole().then(() => { + switch (this.userRole) { + case 'Student': this.$router.push('/student') + break + case 'Tutor': this.$router.push('/tutor') + break + } + }) this.getJWTTimeDelta() this.loading = false - this.$router.push('/student/') }).catch(() => { this.loading = false }) } } diff --git a/frontend/src/pages/SubmissionCorrectionPage.vue b/frontend/src/pages/SubmissionCorrectionPage.vue new file mode 100644 index 00000000..386e8210 --- /dev/null +++ b/frontend/src/pages/SubmissionCorrectionPage.vue @@ -0,0 +1,81 @@ +<template> + <v-layout row wrap> + <v-flex xs12 md6> + <annotated-submission + :rawSubmission="mockSubmission" + :feedback="mockFeedback" + :score="mockScore" + :editable="true" + class="ma-4 autofocus" + /> + </v-flex> + + <v-flex md6> + <submission-type + v-bind="mockSubType" + :reverse="true" + :expandedByDefault="{ Description: false, Solution: true }" + /> + </v-flex> + </v-layout> +</template> + +<script> + import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission' + import SubmissionType from '@/components/SubmissionType' + + export default { + components: { + SubmissionType, + AnnotatedSubmission}, + name: 'submission-correction-page', + data () { + return { + mockSubmission: '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' + + '#include <iostream>\n' + + '#include <iomanip>\n' + + '\n' + + 'using namespace std;\n' + + '\n' + + '\n' + + 'int** comb(int** a , int row , int col)\n' + + '{\n' + + ' int mid = col/2;\n' + + ' //clear matrix\n' + + ' for( int i = 0 ; i < row ; i++)\n' + + ' for( int j = 0 ; j < col ; j++)\n' + + ' a[i][j] = 0;\n' + + ' a[0][mid] = 1; //put 1 in the middle of first row\n' + + ' //build up Pascal\'s Triangle matrix\n' + + ' for( int i = 1 ; i < row ; i++)\n' + + ' {\n' + + ' for( int j = 1 ; j < col - 1 ; j++)\n' + + ' a[i][j] = a[i-1][j-1] + a[i-1][j+1];\n' + + ' }\n' + + ' return a;\n' + + '}\n' + + 'void disp(int** ptr, int row, int col)\n' + + '{\n' + + ' cout << endl << endl;\n' + + ' for ( int i = 0 ; i < row ; i++)\n' + + ' {\n' + + ' for ( int j = 0 ; j < col ; j++)\n', + mockFeedback: { + 1: 'Youre STUPID', + 4: 'Very much so' + }, + mockScore: 42, + mockSubType: { + description: 'Space suits meet with devastation! The vogon dies disconnection like an intelligent dosi.', + solution: 'The volume is a remarkable sinner.', + name: 'Seas stutter from graces like wet clouds.', + fullScore: 42 + } + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/pages/student/StudentLayout.vue b/frontend/src/pages/student/StudentLayout.vue index 28bcabe7..c20f0943 100644 --- a/frontend/src/pages/student/StudentLayout.vue +++ b/frontend/src/pages/student/StudentLayout.vue @@ -19,6 +19,7 @@ </v-list-tile> <v-divider></v-divider> + <v-card color="grey lighten-2" v-if="!mini"> <v-card-title primary-title> <exam-information :exam="exam"></exam-information> diff --git a/frontend/src/pages/tutor/TutorLayout.vue b/frontend/src/pages/tutor/TutorLayout.vue new file mode 100644 index 00000000..b5641050 --- /dev/null +++ b/frontend/src/pages/tutor/TutorLayout.vue @@ -0,0 +1,57 @@ +<template> + <base-layout @sidebarMini="mini = $event"> + + <template slot="header"> + Collapse + </template> + + <v-list dense slot="sidebar-content"> + <v-list-tile exact v-for="(item, i) in generalNavItems" :key="i" :to="item.route"> + <v-list-tile-action> + <v-icon>{{ item.icon }}</v-icon> + </v-list-tile-action> + <v-list-tile-content> + <v-list-tile-title> + {{ item.name }} + </v-list-tile-title> + </v-list-tile-content> + </v-list-tile> + + <v-divider></v-divider> + + + </v-list> + </base-layout> +</template> + + +<script> + import BaseLayout from '@/components/BaseLayout' + + export default { + components: {BaseLayout}, + name: 'tutor-layout', + data () { + return { + generalNavItems: [ + { + name: 'Overview', + icon: 'home', + route: '/tutor' + }, + { + name: 'Progress', + icon: 'trending_up', + route: '/tutor' + }, + { + name: 'Assignments', + icon: 'assignment_turned_in', + route: '/tutor' + } + ] + } + } + } +</script> + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 9ff1b839..babc257c 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,9 +2,11 @@ import Vue from 'vue' import Router from 'vue-router' import store from '../store/store' import Login from '@/pages/Login' +import TutorLayout from '@/pages/tutor/TutorLayout' import StudentPage from '@/pages/student/StudentPage' -import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage' import StudentLayout from '@/pages/student/StudentLayout' +import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage' +import SubmissionCorrectionPage from '@/pages/SubmissionCorrectionPage' import ReviewerPage from '@/pages/reviewer/ReviewerPage' import StudentListOverview from '@/pages/reviewer/StudentListOverview' @@ -30,7 +32,16 @@ const router = new Router({ component: StudentSubmissionPage } ] - + }, + { + path: '/tutor/', + component: TutorLayout, + children: [ + { + path: 'assignment/', + component: SubmissionCorrectionPage + } + ] }, { path: '/reviewer/', diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 0eb0f5fb..5b60e770 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -23,6 +23,15 @@ const store = new Vuex.Store({ getters: { gradySpeak: () => { return gradySays[Math.floor(Math.random() * gradySays.length)] + }, + isStudent: state => { + return state.userRole === 'Student' + }, + isTutor: state => { + return state.userRole === 'Tutor' + }, + isReviewer: state => { + return state.userRole === 'Reviewer' } }, mutations: { @@ -81,8 +90,9 @@ const store = new Vuex.Store({ context.commit('SET_JWT_TIME_DELTA', response.data.timeDelta) }) }, - getUserRole (context) { - ax.get('api/user-role/').then(response => context.commit('SET_USER_ROLE', response.data.role)) + async getUserRole (context) { + const response = await ax.get('api/user-role/') + context.commit('SET_USER_ROLE', response.data.role) }, logout (store) { store.commit('LOGOUT') diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1e43569c..1098822b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5782,6 +5782,10 @@ uuid@^3.0.0, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +v-clipboard@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/v-clipboard/-/v-clipboard-1.0.4.tgz#ffd423484c61b81685d7ea23f2abd2c0e25a7de0" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" -- GitLab