diff --git a/backend/.gitignore b/backend/.gitignore index 438bae1f35383978b491783079cc5ab16c0546a6..5c0818e6e7b1c607cdf992d72b424cba4c540baf 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -7,6 +7,7 @@ __pycache__ MANIFEST .coverage cache/ +/public/ # Django specific dist/ @@ -25,3 +26,6 @@ env/ scripts/ *.csv .importer* + +# node +node_modules diff --git a/frontend/config/index.js b/frontend/config/index.js index 2c458f66802f5f8d206f4f136a0b5d52b59b4c21..91cda89ee4dffe53254ba613294b6df8b9d1bc4b 100644 --- a/frontend/config/index.js +++ b/frontend/config/index.js @@ -17,7 +17,7 @@ module.exports = { // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin - productionGzip: false, + productionGzip: true, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: diff --git a/frontend/index.html b/frontend/index.html index fb63c5378340aee23aac55a9168ec7067cb63066..c2b9135d7c4243df5b2e5ae26ecb8616aeae41f8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ <html> <head> <meta charset="utf-8"> - <title>frontend</title> + <title>Grady</title> </head> <body> <div id="app"></div> diff --git a/frontend/package.json b/frontend/package.json index e657774de9bfafaaaf987fd6ad46799fa65c1307..22197151e318412c0517792f3acc4f3b35ecb54e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ }, "dependencies": { "axios": "^0.17.0", + "font-awesome": "^4.7.0", + "google-code-prettify": "^1.0.5", "vue": "^2.5.2", "vue-router": "^3.0.1", "vuetify": "^0.16.9", @@ -31,6 +33,7 @@ "babel-register": "^6.22.0", "chai": "^4.1.2", "chalk": "^2.0.1", + "compression-webpack-plugin": "^1.0.1", "connect-history-api-fallback": "^1.3.0", "copy-webpack-plugin": "^4.0.1", "cross-env": "^5.0.1", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 64fcd76cf6a10610db451677e80912d837ffaeae..5b8aa4f3057eac1a514b79a64f2946f994971fcc 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,9 +1,9 @@ <template> - <div id="app"> - <v-app> + <v-app> + <div id="app"> <router-view/> - </v-app> - </div> + </div> + </v-app> </template> <script> @@ -11,11 +11,11 @@ name: 'app', components: { } -} + } </script> <style> -#app { + #app { -} + } </style> diff --git a/frontend/src/components/submission_notes/AnnotatedSubmission.vue b/frontend/src/components/submission_notes/AnnotatedSubmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..6b7d35d83f9c995dc5d9e19f73b13737ba9b0b6f --- /dev/null +++ b/frontend/src/components/submission_notes/AnnotatedSubmission.vue @@ -0,0 +1,103 @@ +<template> + <table> + <tr v-for="(code, index) in submission" :key="index"> + <td class="line-number-cell"> + <v-tooltip left close-delay="20" color="transparent" content-class="comment-icon"> + <v-btn block class="line-number-btn" slot="activator" @click="toggleEditorOnLine(index)">{{ index }}</v-btn> + <v-icon small color="indigo accent-3" class="comment-icon">fa-comment</v-icon> + </v-tooltip> + </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]" + @collapseFeedbackForm="showEditorOnLine[index] = false" + :feedback="feedback[index]" + :index="index"> + </comment-form> + </td> + </tr> + </table> +</template> + + +<script> + import {mapGetters, mapState} from 'vuex' + import CommentForm from '@/components/submission_notes/FeedbackForm.vue' + import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue' + + export default { + components: { + FeedbackComment, + CommentForm}, + name: 'annotated-submission', + beforeCreate () { + this.$store.dispatch('getFeedback', 0) + this.$store.dispatch('getSubmission', 0) + }, + computed: { + ...mapState({ + feedback: state => state.submissionNotes.feedback + }), + ...mapGetters(['submission']) + }, + data: function () { + return { + showEditorOnLine: { } + } + }, + methods: { + toggleEditorOnLine (lineIndex) { + this.$set(this.showEditorOnLine, lineIndex, !this.showEditorOnLine[lineIndex]) + } + }, + mounted () { + window.PR.prettyPrint() + } + } +</script> + + +<style scoped> + + table { + table-layout: auto; + border-collapse: collapse; + border-width: 0px; + } + + td { + /*white-space: nowrap;*/ + /*border: 1px solid green;*/ + } + + .line-number-cell { + padding-left: 50px; + vertical-align: top; + } + + pre.prettyprint { + padding: 0px; + border: 0px; + } + + code { + width: 100%; + } + + + .line-number-btn { + height: fit-content; + min-width: fit-content; + margin: 0; + } + + .comment-icon { + border: 0px; + } + +</style> diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue new file mode 100644 index 0000000000000000000000000000000000000000..a63b4de6a32267608763b9f999f5088adf462e71 --- /dev/null +++ b/frontend/src/components/submission_notes/FeedbackComment.vue @@ -0,0 +1,54 @@ +<template> + <div class="dialogbox"> + <div class="body"> + <span class="tip tip-up"></span> + <div class="message"> + <slot></slot> + </div> + </div> + </div> +</template> + + +<script> + export default { + name: 'feedback-comment' + } +</script> + + +<style scoped> + .tip { + width: 0px; + height: 0px; + position: absolute; + background: transparent; + border: 10px solid #3D8FC1; + } + + .tip-up { + top: -25px; /* Same as body margin top + border */ + left: 10px; + border-right-color: transparent; + border-left-color: transparent; + border-top-color: transparent; + } + + .dialogbox .body { + position: relative; + height: auto; + margin: 20px 10px 10px 10px; + padding: 5px; + background-color: #F3F3F3; + border-radius: 5px; + border: 5px solid #3D8FC1; + } + + .body .message { + font-family: Roboto, sans-serif; + min-height: 30px; + border-radius: 3px; + font-size: 14px; + line-height: 1.5; + } +</style> diff --git a/frontend/src/components/submission_notes/FeedbackForm.vue b/frontend/src/components/submission_notes/FeedbackForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..7813c0be9323b110ad7a19518de65c1f35a7fa60 --- /dev/null +++ b/frontend/src/components/submission_notes/FeedbackForm.vue @@ -0,0 +1,54 @@ +<template> + <div> + <v-text-field + name="feedback-input" + label="Please provide your feedback here" + v-model="current_feedback" + @keyup.enter.ctrl.exact="submitFeedback" + @keyup.esc="collapseTextField" + 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> +</template> + + +<script> + export default { + name: 'comment-form', + props: ['feedback', 'index'], + data () { + return { + current_feedback: this.feedback + } + }, + methods: { + + collapseTextField () { + this.$emit('collapseFeedbackForm') + }, + submitFeedback () { + this.$store.dispatch('updateFeedback', { + lineIndex: this.index, + content: this.current_feedback + }) + this.collapseTextField() + }, + discardFeedback () { + this.current_feedback = this.feedback + } + } + } +</script> + + +<style scoped> + v-text-field { + padding-top: 0px; + } +</style> diff --git a/frontend/src/main.js b/frontend/src/main.js index 997422b82770d3b595a39127ea98f36215f9c98c..2181166f03cba804c434612faeae486d6d520b73 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -7,6 +7,9 @@ import store from './store/store' import Vuetify from 'vuetify' import 'vuetify/dist/vuetify.min.css' +import 'font-awesome/css/font-awesome.min.css' +import 'google-code-prettify/bin/prettify.min' +import 'google-code-prettify/bin/prettify.min.css' Vue.use(Vuetify) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index c29cbc7da105a411223133581fbde9ddec64aace..344754b8d4ea418c1cb8465f475a9777a8f49034 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,6 +2,7 @@ import Vue from 'vue' import Router from 'vue-router' import Login from '@/components/Login' import StudentPage from '@/components/student/StudentPage' +import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission' Vue.use(Router) @@ -16,6 +17,11 @@ export default new Router({ path: '/student/', name: 'student-page', component: StudentPage + }, + { + path: '/notes/', + name: 'annotated-submission', + component: AnnotatedSubmission } ] }) diff --git a/frontend/src/store/api.js b/frontend/src/store/api.js index 368c09c86195ded2f5c97b1199a26e37c4184190..2be0ed429deada34d26903031a564dbf31afde6a 100644 --- a/frontend/src/store/api.js +++ b/frontend/src/store/api.js @@ -1,6 +1,6 @@ import axios from 'axios' -var ax = axios.create({ +let ax = axios.create({ baseURL: 'http://localhost:8000/' }) diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.js new file mode 100644 index 0000000000000000000000000000000000000000..cde09fa0c39e2f4a38869dc8746aee0c3be86628 --- /dev/null +++ b/frontend/src/store/modules/submission-notes.js @@ -0,0 +1,76 @@ +import Vue from 'vue' + +const mockSubmission = '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' + + '#include <iostream>\n' + + '#include <iomanip>\n' + + 'using namespace std;\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' + +const mockFeedback = { + '1': 'Youre STUPID', + '4': 'Very much so' +} + +const submissionNotes = { + state: { + rawSubmission: '', + feedback: {} + }, + getters: { + // reduce the string rawSubmission into an object where the keys are the + // line indexes starting at one and the values the corresponding submission line + // this makes iterating over the submission much more pleasant + submission: state => { + return state.rawSubmission.split('\n').reduce((acc, cur, index) => { + acc[index + 1] = cur + return acc + }, {}) + } + }, + mutations: { + 'SET_RAW_SUBMISSION': function (state, submission) { + state.rawSubmission = mockSubmission + }, + 'SET_FEEDBACK': function (state, feedback) { + state.feedback = feedback + }, + 'UPDATE_FEEDBACK': function (state, feedback) { + Vue.set(state.feedback, feedback.lineIndex, feedback.content) + } + }, + actions: { + // TODO remove mock data + getSubmission (context, submissionId) { + context.commit('SET_RAW_SUBMISSION', mockSubmission) + }, + getFeedback (context, feedbackId) { + context.commit('SET_FEEDBACK', mockFeedback) + }, + updateFeedback (context, lineIndex, feedbackContent) { + context.commit('UPDATE_FEEDBACK', lineIndex, feedbackContent) + } + } +} + +export default submissionNotes diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 150521a07241ce031fa1bd5454e3ba7a9e9f963f..9e1e6c4396d1ef49ea7e4dcce68422067acab48a 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -2,19 +2,27 @@ import Vuex from 'vuex' import Vue from 'vue' import ax from './api' +import submissionNotes from './modules/submission-notes' + Vue.use(Vuex) const store = new Vuex.Store({ + modules: { + submissionNotes: submissionNotes + }, state: { token: '', loggedIn: false, username: '' }, mutations: { - 'LOGIN': function (state, creds) { - state.token = creds.token + 'SET_JWT_TOKEN': function (state, token) { + state.token = token + ax.defaults.headers.common['Authorization'] = 'JWT ' + state.token + }, + 'LOGIN': function (state, credentials) { + state.username = credentials.username state.loggedIn = true - state.username = creds.username }, 'LOGOUT': function (state) { state.token = '' @@ -22,16 +30,12 @@ const store = new Vuex.Store({ } }, actions: { - async getToken (store, credentials) { + async getToken (context, credentials) { const response = await ax.post('api-token-auth/', credentials) - store.commit('LOGIN', { - token: response.data.token, + context.commit('LOGIN', { username: credentials.username }) - ax.defaults.headers.common['Authorization'] = 'JWT ' + response.data.token - }, - logout (store) { - store.commit('LOGOUT') + context.commit('SET_JWT_TOKEN', response.data.token) } } }) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a73065fa651effc5cd424b1f4e3d89efebdb00a9..5c6178fed14c2cbdb9c39b9653c67a29c61a6329 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -228,6 +228,12 @@ async@1.x, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" +async@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" + dependencies: + lodash "^4.14.0" + async@^2.1.2, async@^2.4.1: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" @@ -1355,6 +1361,13 @@ component-inherit@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" +compression-webpack-plugin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.0.1.tgz#7f0a2af9f642b4f87b5989516a3b9e9b41bb4b3f" + dependencies: + async "2.4.1" + webpack-sources "^1.0.1" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2477,6 +2490,10 @@ follow-redirects@^1.2.3: dependencies: debug "^2.6.9" +font-awesome@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -2682,6 +2699,10 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +google-code-prettify@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/google-code-prettify/-/google-code-prettify-1.0.5.tgz#9f477f224dbfa62372e5ef803a7e157410400084" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"