diff --git a/.gitignore b/.gitignore index d25fbf419..ac1e86c6c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ build/ .env.test.local .env.production.local +debug.log* npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/backend/app.js b/backend/app.js index fc9231c79..c9088576e 100755 --- a/backend/app.js +++ b/backend/app.js @@ -2,6 +2,11 @@ let express = require('express') let app = express() const jwt = require('jsonwebtoken') const bodyParser = require('body-parser') +const Raven = require('raven') +const logger = require('./server/utils/logger') + +Raven.config(process.env.SENTRY_ADDR).install() + require('dotenv').config() /** @@ -106,10 +111,9 @@ const authenticate = (request, response, next) => { try { let decoded = jwt.verify(request.token, process.env.SECRET) ;(request.decoded = decoded), (request.authenticated = { success: true, error: '' }) - console.log(' Authenticated: true') } catch (e) { request.authenticated = { success: false, error: 'token verification failed' } - console.log(' Authenticated: false') + logger.error(e) } } diff --git a/backend/package-lock.json b/backend/package-lock.json index 2bfcd9f54..3d9c9ed8b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -265,6 +265,14 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "4.17.10" + } + }, "async-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", @@ -655,6 +663,11 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -804,6 +817,15 @@ "object-visit": "1.0.1" } }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "1.9.1", + "color-string": "1.5.3" + } + }, "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", @@ -817,6 +839,34 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "1.1.3", + "simple-swizzle": "0.2.2" + } + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "colors": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz", + "integrity": "sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==" + }, + "colorspace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.1.tgz", + "integrity": "sha512-pI3btWyiuz7Ken0BWh9Elzsmv2bM9AhA7psXib4anUXy/orfZ/E0MbQwhSOG/9L8hLlalqrU0UhOuqxW1YjmVw==", + "requires": { + "color": "3.0.0", + "text-hex": "1.0.0" + } + }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -970,21 +1020,31 @@ } } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.2.tgz", + "integrity": "sha512-U2ALcoAHvA1oO2xOreyHvtkQ+IELqDG2WVWRI1GH/XEmmfGIOalnM5MU5Dd2ITyWfr3m6kNqXiy8XuYyd4wKJw==", "requires": { - "boom": "5.2.0" + "boom": "7.2.0" }, "dependencies": { "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", + "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", "requires": { - "hoek": "4.2.1" + "hoek": "5.0.4" } + }, + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" } } }, @@ -1153,6 +1213,16 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "requires": { + "colorspace": "1.1.1", + "enabled": "1.0.2", + "kuler": "1.0.0" + } + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -1236,11 +1306,24 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.4" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "env-variable": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.4.tgz", + "integrity": "sha512-+jpGxSWG4vr6gVxUHOc4p+ilPnql7NzZxOZBxNldsKGjCF+97df3CbuX7XMaDa5oAVkKQj4rKp38rYdC4VcpDg==" + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -1916,6 +1999,16 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -2775,6 +2868,26 @@ "cryptiles": "3.1.2", "hoek": "4.2.1", "sntp": "2.1.0" + }, + "dependencies": { + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + } + } + } } }, "he": { @@ -3388,6 +3501,14 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, + "kuler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.0.tgz", + "integrity": "sha512-oyy6pu/yWRjiVfCoJebNUKFL061sNtrs9ejKTbirIwY3oiHmENVCSkHhxDV85Dkm7JYR/czMCBeoM87WilTdSg==", + "requires": { + "colornames": "1.1.1" + } + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -3484,6 +3605,25 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "logform": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.9.1.tgz", + "integrity": "sha512-ZHrZE8VSf7K3xKxJiQ1aoTBp2yK+cEbFcgarsjzI3nt3nE/3O0heNSppoOQMUJVMZo/xiVwCxiXIabaZApsKNQ==", + "requires": { + "colors": "1.3.2", + "fast-safe-stringify": "2.0.6", + "fecha": "2.3.3", + "ms": "2.1.1", + "triple-beam": "1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -3552,6 +3692,16 @@ "object-visit": "1.0.1" } }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "1.1.6" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3725,16 +3875,16 @@ } }, "moment": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", - "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" }, "moment-timezone": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.15.tgz", "integrity": "sha512-5KJF9RGDKwZDcIpkb3vOJ1e7JFPsA+d4Ni+EM6NQFB3h4XlTmZSrqSjtJo7T4anyGgYYb3/GwqCzJ66BkL4GtQ==", "requires": { - "moment": "2.22.1" + "moment": "2.22.2" } }, "morgan": { @@ -4024,6 +4174,11 @@ "wrappy": "1.0.2" } }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, "onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -4609,6 +4764,25 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, + "raven": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/raven/-/raven-2.6.4.tgz", + "integrity": "sha512-6PQdfC4+DQSFncowthLf+B6Hr0JpPsFBgTVYTAOq7tCmx/kR4SXbeawtPch20+3QfUcQDoJBLjWW1ybvZ4kXTw==", + "requires": { + "cookie": "0.3.1", + "md5": "2.2.1", + "stack-trace": "0.0.10", + "timed-out": "4.0.1", + "uuid": "3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "raw-body": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", @@ -4947,7 +5121,7 @@ "generic-pool": "3.4.2", "inflection": "1.12.0", "lodash": "4.17.10", - "moment": "2.22.1", + "moment": "2.22.2", "moment-timezone": "0.5.15", "retry-as-promised": "2.3.2", "semver": "5.5.0", @@ -5123,6 +5297,21 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "0.3.2" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", @@ -5322,6 +5511,11 @@ "tweetnacl": "0.14.5" } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, "standard": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/standard/-/standard-11.0.1.tgz", @@ -5710,6 +5904,11 @@ "terraformer": "1.0.8" } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5823,6 +6022,11 @@ "punycode": "1.4.1" } }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6170,6 +6374,87 @@ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" }, + "winston": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.1.0.tgz", + "integrity": "sha512-FsQfEE+8YIEeuZEYhHDk5cILo1HOcWkGwvoidLrDgPog0r4bser1lEIOco2dN9zpDJ1M88hfDgZvxe5z4xNcwg==", + "requires": { + "async": "2.6.1", + "diagnostics": "1.1.1", + "is-stream": "1.1.0", + "logform": "1.9.1", + "one-time": "0.0.4", + "readable-stream": "2.3.6", + "stack-trace": "0.0.10", + "triple-beam": "1.3.0", + "winston-transport": "4.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "winston-log2gelf": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/winston-log2gelf/-/winston-log2gelf-2.0.3.tgz", + "integrity": "sha512-ycduUIqX/FGz9AKiFwjMEjKw/2uIFoeWr2uqHHyk0zFrQ4LMeJKONAIF3xPSS+UkL7DpKtDTOZn5ReD4tVRsjA==", + "requires": { + "winston-transport": "4.2.0" + } + }, + "winston-transport": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.2.0.tgz", + "integrity": "sha512-0R1bvFqxSlK/ZKTH86nymOuKv/cT1PQBMuDdA7k7f0S9fM44dNH6bXnuxwXPrN8lefJgtZq08BKdyZ0DZIy/rg==", + "requires": { + "readable-stream": "2.3.6", + "triple-beam": "1.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, "with": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 1b93496b9..e07f29fb2 100755 --- a/backend/package.json +++ b/backend/package.json @@ -7,11 +7,13 @@ "axios": "^0.18.0", "body-parser": "^1.18.2", "cors": "^2.8.4", + "cryptiles": "^4.1.2", "dotenv": "^5.0.1", "express": "^4.16.3", "express-basic-auth": "^1.1.4", "fs": "0.0.1-security", "jsonwebtoken": "^8.2.1", + "moment": "^2.22.2", "morgan": "^1.9.0", "nodemailer": "^4.6.7", "nodemon": "^1.17.5", @@ -19,11 +21,14 @@ "pg": "^7.4.1", "pg-hstore": "^2.3.2", "pug": "^2.0.3", + "raven": "^2.6.4", "request": "^2.85.0", "sequelize": "^4.37.6", "sequelize-cli": "^4.0.0", "should": "^13.2.1", - "tape": "^4.9.0" + "tape": "^4.9.0", + "winston": "^3.1.0", + "winston-log2gelf": "^2.0.3" }, "devDependencies": { "assert": "^1.4.1", diff --git a/backend/server/controllers/courseinstances.js b/backend/server/controllers/courseinstances.js index 677117628..3836f4f17 100755 --- a/backend/server/controllers/courseinstances.js +++ b/backend/server/controllers/courseinstances.js @@ -14,12 +14,18 @@ const Tag = require('../models').Tag const Checklist = require('../models').Checklist const env = process.env.NODE_ENV || 'development' const config = require('./../config/config.js')[env] +const logger = require('../utils/logger') const overkillLogging = (req, error) => { console.log('request: ', req) console.log('error: ', error) } +const validationErrorMessages = { + github: 'Github repository link is not a proper url.', + projectName: 'Project name contains illegal characters.\nCharacters allowed are letters from a-ö, numbers, apostrophe, - and whitespace (not multiple in a row or at first/last character)' +} + module.exports = { /** * @@ -34,7 +40,10 @@ module.exports = { db.sequelize .query(`SELECT * FROM "CourseInstances" JOIN "TeacherInstances" ON "CourseInstances"."id" = "TeacherInstances"."courseInstanceId" WHERE "TeacherInstances"."userId" = ${req.decoded.id}`) .then(instance => res.status(200).send(instance[0])) - .catch(error => res.status(400).send(error)) + .catch((error) => { + logger.error(error) + res.status(400).send(error) + }) }, /** * @@ -234,6 +243,12 @@ module.exports = { model: Tag, attributes: ['id', 'name', 'color'] } + ], + order: [ + [ + { model: CodeReview, as: 'codeReviews' }, + 'reviewNumber', 'ASC' + ] ] }) try { @@ -321,10 +336,6 @@ module.exports = { }) } catch (error) { if (error.name === 'SequelizeValidationError') { - const validationErrorMessages = { - github: 'Github repository link is not a proper url.', - projectName: 'Project name contains illegal characters.' - } const errorMessage = error.errors.map(e => validationErrorMessages[e.path] || 'Unknown validation error.') return res.status(400).json({ message: errorMessage.join('\n') @@ -391,15 +402,22 @@ module.exports = { console.log('\nUpdated student project info succesfully\n') res.status(200).send(updatedStudentInstance) }) - .catch(error => { - console.log('\nerror happened\n') - res.status(400).send('update failed') + .catch((error) => { + if (error.name === 'SequelizeValidationError') { + const errorMessage = error.errors.map(e => validationErrorMessages[e.path] || 'Unknown validation error.') + logger.error(error) + return res.status(400).send({ message: errorMessage.join('\n') }) + } }) }) }) - .catch(error => res.status(400).send('\n\n\n\ntuli joku error: ', error)) + .catch(error => { + logger.error(error) + res.status(400).send('\n\n\n\ntuli joku error: ', error) + }) } } catch (e) { + logger.error(e) res.status(400).send(e) } }, @@ -449,11 +467,20 @@ module.exports = { currentCodeReview: req.body.newCr.length === 0 ? '{}' : req.body.newCr }) .then(updatedCourseInstance => res.status(200).send(updatedCourseInstance)) - .catch(error => res.status(400).send(error)) + .catch(error => { + res.status(400).send(error) + logger.error(error) + }) + }) + .catch(error => { + res.status(400).send(error) + logger.error(error) }) - .catch(error => res.status(400).send(error)) }) - .catch(error => res.status(400).send(error)) + .catch(error => { + res.status(400).send(error) + logger.error(error) + }) }, /** @@ -469,7 +496,10 @@ module.exports = { } }) .then(instance => res.status(200).send(instance)) - .catch(error => res.status(400).send(error)) + .catch(error => { + logger.error(error) + res.status(400).send(error) + }) }, /** @@ -500,7 +530,10 @@ module.exports = { } return res.status(200).send(courseInstance) }) - .catch(error => res.status(400).send(error)) + .catch(error => { + logger.error(error) + res.status(400).send(error) + }) }, /** * @@ -704,7 +737,10 @@ module.exports = { res.status(200).send(comment) } }) - .catch(error => res.status(400).send(error)) + .catch(error => { + res.status(400).send(error) + logger.error(error) + }) } }) } catch (e) { @@ -731,6 +767,9 @@ module.exports = { } }) .then(comment => res.status(200).send(comment)) - .catch(error => res.status(400).send(error)) + .catch(error => { + res.status(400).send(error) + logger.error(error) + }) } } diff --git a/backend/server/controllers/tags.js b/backend/server/controllers/tags.js index d5f6f630f..fa351459a 100644 --- a/backend/server/controllers/tags.js +++ b/backend/server/controllers/tags.js @@ -7,6 +7,7 @@ const User = require('../models').User const Comment = require('../models').Comment const CodeReview = require('../models').CodeReview const helper = require('../helpers/course_instance_helper') +const logger = require('../../server/utils/logger') module.exports = { /** @@ -51,6 +52,7 @@ module.exports = { return }) .catch(error => { + logger.error(error) res.status(400).send('color did not update') return }) @@ -100,7 +102,10 @@ module.exports = { .then(tag => { return res.status(200).send(tag) }) - .catch(error => res.status(400).send('et ny saa niitä tageja')) + .catch(error => { + res.status(400).send('et ny saa niitä tageja') + logger.error(error) + }) } catch (e) { res.status(400).send('nymmeni jokin pieleen') return diff --git a/backend/server/controllers/users.js b/backend/server/controllers/users.js index 07d0677b7..3a45e9718 100755 --- a/backend/server/controllers/users.js +++ b/backend/server/controllers/users.js @@ -3,6 +3,7 @@ const jwt = require('jsonwebtoken') const CourseInstance = require('../models').CourseInstance const TeacherInstance = require('../models').TeacherInstance const helper = require('../helpers/users_controller_helper') +const logger = require('../utils/logger') function invalidInputResponse(res, error) { res.status(400).send({ error }) @@ -39,7 +40,10 @@ module.exports = { } res.status(201).send(returnedUser) }) - .catch(error => res.status(400).send(error)) + .catch(error => { + res.status(400).send(error) + logger.error(error) + }) ) } }, @@ -73,6 +77,7 @@ module.exports = { }) res.status(200).send(users) } catch (exception) { + logger.error(exception) res.status(400).send('Unable to send user list') } } diff --git a/backend/server/controllers/weeks.js b/backend/server/controllers/weeks.js index f65ef7b0e..ba0d2b14e 100644 --- a/backend/server/controllers/weeks.js +++ b/backend/server/controllers/weeks.js @@ -2,6 +2,7 @@ const Week = require('../models').Week const TeacherInstance = require('../models').TeacherInstance const StudentInstance = require('../models').StudentInstance const helper = require('../helpers/weeks_controller_helper') +const logger = require('../utils/logger') module.exports = { async create(req, res) { @@ -81,7 +82,10 @@ module.exports = { return Week.all() .then(ui => res.status(200).send(ui)) - .catch(error => res.status(400).send(error)) + .catch(error => { + logger.error(error) + res.status(400).send(error) + }) }, /** * @@ -101,6 +105,9 @@ module.exports = { } return res.status(200).send(week) }) - .catch(error => res.status(400).send(error)) + .catch(error => { + logger.error(error) + res.status(400).send(error) + }) } } diff --git a/backend/server/helpers/course_instance_helper.js b/backend/server/helpers/course_instance_helper.js index 4ec02b075..b500719e0 100755 --- a/backend/server/helpers/course_instance_helper.js +++ b/backend/server/helpers/course_instance_helper.js @@ -1,6 +1,8 @@ const application_helpers = require('./application_helper') + const env = process.env.NODE_ENV || 'development' const config = require('./../config/config.js')[env] +const logger = require('../utils/logger') exports.CurrentTermAndYear = application_helpers.CurrentTermAndYear exports.getCurrentTerm = application_helpers.getCurrentTerm @@ -32,11 +34,12 @@ function checkWebOodi(req, res, user, resolve) { if (process.env.INCLUDE_TESTERS) { options.uri += '?testing=1' } - request(options, function(req, res, body) { + request(options, function (req, res, body) { let json = null try { json = JSON.parse(body) } catch (e) { + logger.error(e) resolve('notfound') return } @@ -74,7 +77,10 @@ function findByUserStudentInstance(req, res) { db.sequelize .query(`SELECT * FROM "CourseInstances" JOIN "StudentInstances" ON "CourseInstances"."id" = "StudentInstances"."courseInstanceId" WHERE "StudentInstances"."userId" = ${req.decoded.id}`) .then(instance => res.status(200).send(instance[0])) - .catch(error => res.status(400).send(error)) + .catch((error) => { + logger.error(error) + res.status(400).send(error) + }) } else { errors.push('\nsomething went wrong') res.status(400).send(errors) diff --git a/backend/server/models/studentinstance.js b/backend/server/models/studentinstance.js index f884a4d57..a3988dc49 100755 --- a/backend/server/models/studentinstance.js +++ b/backend/server/models/studentinstance.js @@ -13,7 +13,7 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.STRING, allowNull: false, validate: { - is: ['^[a-zåäöA-ZÅÄÖ\'\\-0-9]*$'] + is: ['^[a-zåäöA-ZÅÄÖ\'\\-0-9]+( [a-zåäöA-ZÅÄÖ\'\\-0-9]+)*$'] } } }, diff --git a/backend/server/utils/logger.js b/backend/server/utils/logger.js new file mode 100644 index 000000000..7faa48051 --- /dev/null +++ b/backend/server/utils/logger.js @@ -0,0 +1,30 @@ +const Log2gelf = require('winston-log2gelf') +const winston = require('winston') +const moment = require('moment') + +const { combine, timestamp, prettyPrint } = winston.format +const transports = [] +if (process.env.NODE_ENV !== 'test') { + transports.push(new winston.transports.File({ filename: 'debug.log' })) +} +transports.push(new (winston.transports.Console)()) +if (process.env.LOG_PORT && process.env.LOG_HOST) { + transports.push(new Log2gelf({ + hostname: process.env.LOG_HOSTNAME || 'labtool-backend', + host: process.env.LOG_HOST, + port: process.env.LOG_PORT, + protocol: 'http' + })) +} +const logger = winston.createLogger({ + format: combine( + timestamp({ + format: () => { + return moment().format('DD-MM-YYYY HH:mm:ss') + } + }), + prettyPrint({ depth: 5 }) + ), + transports +}) +module.exports = logger diff --git a/labtool2.0/src/components/BackButton.js b/labtool2.0/src/components/BackButton.js new file mode 100644 index 000000000..c11e85a6e --- /dev/null +++ b/labtool2.0/src/components/BackButton.js @@ -0,0 +1,56 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { Link } from 'react-router-dom' +import { Button, Icon } from 'semantic-ui-react' + +const BackButton = props => ( + + + +) + +BackButton.propTypes = { + to: PropTypes.string, + enabled: PropTypes.bool.isRequired, + text: PropTypes.string.isRequired +} + +const presets = { + modifyCIPage: { + to: state => `/labtool/ModifyCourseInstancePage/${state.selectedInstance.ohid}`, + text: 'Back to course editing' + }, + coursePage: { + to: state => `/labtool/courses/${state.selectedInstance.ohid}`, + text: 'Back to course page' + } +} + +const mapStateToProps = (state, ownProps) => { + let to + try { + to = presets[ownProps.preset].to(state) + } catch (e) { + to = ownProps.to + } + let text + if (presets[ownProps.preset]) { + text = presets[ownProps.preset].text + } else { + text = ownProps.text || 'Back' + } + return { + to, + text, + enabled: to !== undefined + } +} + +export default connect( + mapStateToProps, + null +)(BackButton) diff --git a/labtool2.0/src/components/pages/BrowseReviews.js b/labtool2.0/src/components/pages/BrowseReviews.js index 1d3d29477..73c91a840 100644 --- a/labtool2.0/src/components/pages/BrowseReviews.js +++ b/labtool2.0/src/components/pages/BrowseReviews.js @@ -8,6 +8,9 @@ import { gradeCodeReview } from '../../services/codeReview' import ReactMarkdown from 'react-markdown' import { sendEmail } from '../../services/email' import { resetLoading } from '../../reducers/loadingReducer' +import { trimDate } from '../../util/format' + +import BackButton from '../BackButton' /** * Maps all comments from a single instance from coursePage reducer @@ -62,13 +65,6 @@ export class BrowseReviews extends Component { } } - trimDate = date => { - return new Date(date) - .toLocaleString() - .replace('/', '.') - .replace('/', '.') - } - sortCommentsByDate = comments => { return comments.sort((a, b) => { return new Date(a.createdAt) - new Date(b.createdAt) @@ -129,7 +125,7 @@ export class BrowseReviews extends Component { const weeks = student.weeks.find(week => week.weekNumber === i + 1) if (weeks) { headers.push( - + Week {i + 1}, points {weeks.points} @@ -168,7 +164,7 @@ export class BrowseReviews extends Component { {comment.comment}{' '} -
{this.trimDate(comment.createdAt)}
+
{trimDate(comment.createdAt)}
@@ -181,7 +177,7 @@ export class BrowseReviews extends Component { {comment.comment}{' '} -
{this.trimDate(comment.createdAt)}
+
{trimDate(comment.createdAt)}
{/* This hack compares user's name to comment.from and hides the email notification button when they don't match. */} @@ -219,7 +215,7 @@ export class BrowseReviews extends Component { ) } else { headers.push( - + Week {i + 1}{' '} @@ -240,7 +236,7 @@ export class BrowseReviews extends Component { }) .forEach(cr => { headers.push( - + {' '} Code Review {cr.reviewNumber} {cr.points !== null ? ', points ' + cr.points : ''} @@ -267,7 +263,7 @@ export class BrowseReviews extends Component { const finalWeek = student.weeks.find(week => week.weekNumber === this.props.selectedInstance.weekAmount + 1) if (finalWeek) { headers.push( - + Final Review, points {finalWeek.points} @@ -325,7 +321,7 @@ export class BrowseReviews extends Component { ) } else { headers.push( - + Final Review{' '} @@ -343,7 +339,7 @@ export class BrowseReviews extends Component { } return student }) - return headers + return headers.map((header, index) => React.cloneElement(header, { key: index })) } const { activeIndex } = this.state @@ -353,7 +349,8 @@ export class BrowseReviews extends Component { {this.props.courseData.role === 'teacher' ? (
- + +

{this.props.selectedInstance.name}

{createHeaders(this.props.courseData, this.props.studentInstance)} diff --git a/labtool2.0/src/components/pages/CoursePage.js b/labtool2.0/src/components/pages/CoursePage.js index 545930a40..1bd3f8275 100644 --- a/labtool2.0/src/components/pages/CoursePage.js +++ b/labtool2.0/src/components/pages/CoursePage.js @@ -21,6 +21,7 @@ import { toggleCodeReview } from '../../reducers/coursePageLogicReducer' import { resetLoading } from '../../reducers/loadingReducer' +import { trimDate } from '../../util/format' export class CoursePage extends React.Component { handleClick = (e, titleProps) => { @@ -63,13 +64,6 @@ export class CoursePage extends React.Component { }) } - trimDate = date => { - return new Date(date) - .toLocaleString() - .replace('/', '.') - .replace('/', '.') - } - changeHiddenAssistantDropdown = id => { return () => { this.props.showAssistantDropdown(this.props.coursePageLogic.showAssistantDropdown === id ? '' : id) @@ -213,6 +207,10 @@ export class CoursePage extends React.Component { const numberOfCodeReviews = Array.isArray(this.props.courseData.data) ? Math.max(...this.props.courseData.data.map(student => student.codeReviews.length)) : 0 const createIndents = (weeks, codeReviews, siId) => { + const cr = codeReviews && + codeReviews.reduce((a, b) => { + return { ...a, [b.reviewNumber]: b.points } + }, {}) const indents = [] let i = 0 let finalPoints = undefined @@ -236,19 +234,26 @@ export class CoursePage extends React.Component { } indents.push(pushattava) } + let ii = 0 - codeReviews.forEach(cr => { - indents.push({cr.points !== null ?

{cr.points}

:

-

}
) - ii++ - }) - while (ii < numberOfCodeReviews) { - indents.push( - -

-

-
- ) - ii++ + const { amountOfCodeReviews } = this.props.selectedInstance + if (amountOfCodeReviews) { + for (let index = 1; index <= amountOfCodeReviews; index++) { + indents.push({cr[index] || cr[index] === 0 ?

{cr[index]}

:

-

}
) + } } + // codeReviews.forEach(cr => { + // indents.push({cr.points !== null ?

{cr.points}

:

-

}
) + // ii++ + // // }) + // while (ii < numberOfCodeReviews) { + // indents.push( + // + //

-

+ //
+ // ) + // ii++ + // } if (this.props.selectedInstance.finalReview) { let finalReviewPointsCell = ( @@ -271,7 +276,7 @@ export class CoursePage extends React.Component { for (; i < this.props.selectedInstance.weekAmount; i++) { headers.push(Week {i + 1} ) } - for (var ii = 1; ii <= numberOfCodeReviews; ii++) { + for (var ii = 1; ii <= this.props.selectedInstance.amountOfCodeReviews; ii++) { headers.push(Code Review {ii} ) } if (this.props.selectedInstance.finalReview) { @@ -344,7 +349,7 @@ export class CoursePage extends React.Component { {comment.comment}{' '} -
{this.trimDate(comment.createdAt)}
+
{trimDate(comment.createdAt)}
@@ -357,7 +362,7 @@ export class CoursePage extends React.Component { {comment.comment}{' '} -
{this.trimDate(comment.createdAt)}
+
{trimDate(comment.createdAt)}
{/* This hack compares user's name to comment.from and hides the email notification button when they don't match. */} @@ -501,15 +506,15 @@ export class CoursePage extends React.Component { let renderTeacherTopPart = () => { return (
-
-
+
+

{this.props.selectedInstance.name}

{this.props.courseInstance && this.props.courseInstance.active === true ? ( this.props.courseData.data !== null ? (

) : ( -

+
You have not activated this course. @@ -587,7 +592,7 @@ export class CoursePage extends React.Component { )}
- +
Student @@ -749,7 +754,7 @@ export class CoursePage extends React.Component { ) } else if (this.props.courseData.role === 'teacher') { return ( -
+
{renderTeacherTopPart()} {renderTeacherBottomPart()}
diff --git a/labtool2.0/src/components/pages/CreateChecklist.js b/labtool2.0/src/components/pages/CreateChecklist.js index 08450c52e..caa077644 100644 --- a/labtool2.0/src/components/pages/CreateChecklist.js +++ b/labtool2.0/src/components/pages/CreateChecklist.js @@ -8,6 +8,8 @@ import { getOneCI, getAllCI } from '../../services/courseInstance' import { resetChecklist, changeField, addTopic, addRow, removeTopic, removeRow, castPointsToNumber } from '../../reducers/checklistReducer' import './CreateChecklist.css' +import BackButton from '../BackButton' + export class CreateChecklist extends Component { constructor(props) { super(props) @@ -328,6 +330,7 @@ export class CreateChecklist extends Component { const { checklistJsx, maxPoints } = this.props.loading.loading ? { checklistJsx: null, maxPoints: null } : this.renderChecklist() return (
+
{this.props.selectedInstance.name}
diff --git a/labtool2.0/src/components/pages/ModifyCourseInstanceCodeReviews.js b/labtool2.0/src/components/pages/ModifyCourseInstanceCodeReviews.js index 9e15ddb89..4d52eec3f 100644 --- a/labtool2.0/src/components/pages/ModifyCourseInstanceCodeReviews.js +++ b/labtool2.0/src/components/pages/ModifyCourseInstanceCodeReviews.js @@ -1,5 +1,6 @@ import React from 'react' import { connect } from 'react-redux' +import { Link } from 'react-router-dom' import { getOneCI } from '../../services/courseInstance' import { coursePageInformation } from '../../services/courseInstance' import { bulkinsertCodeReviews, removeOneCodeReview } from '../../services/codeReview' @@ -22,6 +23,8 @@ import { Button, Table, Checkbox, Loader, Dropdown, Label, Popup, Modal, Icon } import Notification from '../../components/pages/Notification' import { resetLoading } from '../../reducers/loadingReducer' +import BackButton from '../BackButton' + export class ModifyCourseInstanceReview extends React.Component { state = { open: {} @@ -211,6 +214,23 @@ export class ModifyCourseInstanceReview extends React.Component { !s[id] ? ((s[id] = true), this.setState({ open: s })) : ((s[id] = !s[id]), this.setState({ open: s })) } + visibilityReminder = () => + this.props.selectedInstance.currentCodeReview && this.props.codeReviewLogic.selectedDropdown ? ( + this.props.selectedInstance.currentCodeReview.findIndex(cr => cr === this.props.codeReviewLogic.selectedDropdown) === -1 ? ( + } + content={ + + This code review is currently not visible to students. You can make it visible on the + course editing page + . + + } + hoverable + /> + ) : null + ) : null + render() { if (this.props.loading.loading) { return @@ -218,6 +238,7 @@ export class ModifyCourseInstanceReview extends React.Component { return (
+

{this.props.selectedInstance.name}


@@ -270,15 +291,17 @@ export class ModifyCourseInstanceReview extends React.Component { Reviewer Project Info - {' '} - 0 ? 'Select code review' : 'No code reviews'} - fluid - options={this.props.dropdownCodeReviews} - /> +
+ + 0 ? 'Select code review' : 'No code reviews'} + fluid + options={this.props.dropdownCodeReviews} + /> +
{this.props.codeReviewLogic.showCreate ? ( diff --git a/labtool2.0/src/components/pages/ModifyCourseInstancePage.js b/labtool2.0/src/components/pages/ModifyCourseInstancePage.js index 0e589347a..6e327d0f9 100644 --- a/labtool2.0/src/components/pages/ModifyCourseInstancePage.js +++ b/labtool2.0/src/components/pages/ModifyCourseInstancePage.js @@ -9,6 +9,8 @@ import { clearNotifications } from '../../reducers/notificationReducer' import { changeCourseField } from '../../reducers/selectedInstanceReducer' import { resetLoading, addRedirectHook } from '../../reducers/loadingReducer' +import BackButton from '../BackButton' + /** * Page used to modify a courseinstances information. Can only be accessed by teachers. */ @@ -87,124 +89,127 @@ export class ModifyCourseInstancePage extends Component { } const selectedInstance = { ...this.props.selectedInstance } return ( -
- - - -

Edit course: {selectedInstance.name}

-
-
- - -
- - - - - - - - - - - - - - - - - - {this.props.selectedInstance.currentCodeReview - ? this.props.selectedInstance.currentCodeReview - .sort((a, b) => { - return a - b - }) - .map( - cr => - this.state.toRemoveCr.includes(cr) ? ( - - {cr} - - } - content={'Click to not be removed on save'} - /> - ) : ( - - {cr} - - } - content={'Click to be removed on save'} - /> - ) - ) - : null} - - - 0 ? 'Select code reviews to set visible' : 'No code reviews'} - /> - - - - - - - - - - - - - - - + } + content={'This code review will be hidden on save'} + /> + ) : ( + + {cr} + + } + content={'Click to hide this code review on save'} + /> + ) + ) + : null} + + + 0 ? 'Select code reviews to set visible' : 'No code reviews'} + /> + + + + + + + + + + + + - - - -
-
- - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + +
) } diff --git a/labtool2.0/src/components/pages/ModifyCourseInstanceStaff.js b/labtool2.0/src/components/pages/ModifyCourseInstanceStaff.js index fb798aa53..7cf4b0b5f 100644 --- a/labtool2.0/src/components/pages/ModifyCourseInstanceStaff.js +++ b/labtool2.0/src/components/pages/ModifyCourseInstanceStaff.js @@ -8,6 +8,8 @@ import { Table, Container, Header, Button, Label, Form, Loader } from 'semantic- import { resetLoading } from '../../reducers/loadingReducer' import { sortUsersByAdminAssistantLastname } from '../../util/sort' +import BackButton from '../BackButton' + export class ModifyCourseInstanceStaff extends React.Component { componentWillMount = async () => { await this.props.resetLoading() @@ -56,6 +58,7 @@ export class ModifyCourseInstanceStaff extends React.Component { } return ( +

Add and remove assistants

{this.props.selectedInstance.name}

diff --git a/labtool2.0/src/components/pages/ReviewStudent.js b/labtool2.0/src/components/pages/ReviewStudent.js index e0af11db3..5f72e850f 100644 --- a/labtool2.0/src/components/pages/ReviewStudent.js +++ b/labtool2.0/src/components/pages/ReviewStudent.js @@ -59,7 +59,7 @@ export class ReviewStudent extends Component { copyChecklistOutput = async e => { e.preventDefault() - this.reviewPointsRef.current.inputRef.value = e.target.points.value + this.reviewPointsRef.current.inputRef.value = Number(e.target.points.value).toFixed(2) /* The below line is as hacky as it is because functional elements cannot directly have refs. * This abomination somehow accesses a textarea that is a child of a div that holds the ref. */ @@ -175,22 +175,23 @@ export class ReviewStudent extends Component { {checkList.list[cl].map(row => ( - - {row.name} -
- - - {row.checkedPoints} p - - - - {row.uncheckedPoints} p - -
+ + + + + + + {row.name} + + + {`${row.checkedPoints} p / ${row.uncheckedPoints} p`} + + +
))} diff --git a/labtool2.0/src/reducers/notificationReducer.js b/labtool2.0/src/reducers/notificationReducer.js index 8980f654f..14c67f338 100644 --- a/labtool2.0/src/reducers/notificationReducer.js +++ b/labtool2.0/src/reducers/notificationReducer.js @@ -188,9 +188,16 @@ const notificationReducer = (state = {}, action) => { } case 'CODE_REVIEW_REMOVE_ONE_SUCCESS': - return { + return { message: 'Code review removed succesfully!', error: false + } + case 'STUDENT_PROJECT_INFO_UPDATE_FAILURE': { + const { message } = action.response.response.data + return { + message, + error: true + } } default: return state diff --git a/labtool2.0/src/tests/__snapshots__/BrowseReviews.test.js.snap b/labtool2.0/src/tests/__snapshots__/BrowseReviews.test.js.snap index d84fd1a3e..a0ec52cdf 100644 --- a/labtool2.0/src/tests/__snapshots__/BrowseReviews.test.js.snap +++ b/labtool2.0/src/tests/__snapshots__/BrowseReviews.test.js.snap @@ -13,8 +13,16 @@ exports[` BrowseReviews Component should render correctly 1`] = active={false} />
+

diff --git a/labtool2.0/src/tests/__snapshots__/CoursePage.test.js.snap b/labtool2.0/src/tests/__snapshots__/CoursePage.test.js.snap index 3db56b477..69ea3ae14 100644 --- a/labtool2.0/src/tests/__snapshots__/CoursePage.test.js.snap +++ b/labtool2.0/src/tests/__snapshots__/CoursePage.test.js.snap @@ -305,7 +305,13 @@ exports[` as student CoursePage Component should render correctly `; exports[` as teacher CoursePage Component should render correctly 1`] = ` -
+
as teacher CoursePage Component should render correctly } } > -
-
+
+

@@ -451,6 +453,8 @@ exports[` as teacher CoursePage Component should render correctly

component should render correctly 1`] = `
+
Aineopintojen harjoitustyö: Tietorakenteet ja algoritmit
diff --git a/labtool2.0/src/tests/__snapshots__/ModifyCourseInstancePage.test.js.snap b/labtool2.0/src/tests/__snapshots__/ModifyCourseInstancePage.test.js.snap index cde026b09..26e8a989d 100644 --- a/labtool2.0/src/tests/__snapshots__/ModifyCourseInstancePage.test.js.snap +++ b/labtool2.0/src/tests/__snapshots__/ModifyCourseInstancePage.test.js.snap @@ -1,339 +1,344 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` Modify Instance Component should render correctly 1`] = ` -
- + - - -

- Edit course: - Aineopintojen harjoitustyö: Tietokantasovellus (periodi IV) - -

-
-
- - -
+ + + - + Edit course: + Aineopintojen harjoitustyö: Tietokantasovellus (periodi IV) + + + + + + + - - - - - + + + - Weekly maxpoints - - - - - + + + - Current week - - - - - + + + - Currently visible code reviews - - - 1 - - } - /> - - 2 - - } - /> - - - - - - - - - + Currently visible code reviews + + + 1 + + } + /> + + 2 + } - } - type="checkbox" - /> - - - - + + + + + + + + - - - - - - - + + + +
+
+ - Add or remove assistant teachers - - - - + + - Add or modify codereviews - - - - + + - Create new checklist - - - - + + - Edit tags - - + + +
`; diff --git a/labtool2.0/src/tests/__snapshots__/ModifyCourseInstanceStaff.test.js.snap b/labtool2.0/src/tests/__snapshots__/ModifyCourseInstanceStaff.test.js.snap index 2bf704f72..2db898db5 100644 --- a/labtool2.0/src/tests/__snapshots__/ModifyCourseInstanceStaff.test.js.snap +++ b/labtool2.0/src/tests/__snapshots__/ModifyCourseInstanceStaff.test.js.snap @@ -2,6 +2,9 @@ exports[` Components renders correctly 1`] = ` +
ReviewStudent Component should render correctly 1`] = key="Koodin laatu" > - - - Koodin laatu - -
- - - p - - - - p - -
+ + + + + + + + Koodin laatu + + + + + undefined p / undefined p + + + +
@@ -212,53 +199,40 @@ exports[` ReviewStudent Component should render correctly 1`] = key="Algoritmin runko" > - - - Algoritmin runko - -
- - - p - - - - p - -
+ + + + + + + + Algoritmin runko + + + + + undefined p / undefined p + + + +
ReviewStudent Component should render correctly 1`] = key="Tietorakenteita luotu" > - - - Tietorakenteita luotu - -
- - - p - - - - p - -
+ + + + + + + + Tietorakenteita luotu + + + + + undefined p / undefined p + + + +
@@ -330,53 +291,40 @@ exports[` ReviewStudent Component should render correctly 1`] = key="Readme" > - - - Readme - -
- - - p - - - - p - -
+ + + + + + + + Readme + + + + + undefined p / undefined p + + + +
ReviewStudent Component should render correctly 1`] = key="Tuntikirjanpito" > - - - Tuntikirjanpito - -
- - - p - - - - p - -
+ + + + + + + + Tuntikirjanpito + + + + + undefined p / undefined p + + + +
diff --git a/labtool2.0/src/util/format.js b/labtool2.0/src/util/format.js new file mode 100644 index 000000000..d278d8867 --- /dev/null +++ b/labtool2.0/src/util/format.js @@ -0,0 +1,10 @@ +const zeros = number => { + const stringForm = number.toString() + return stringForm.length === 1 ? `0${stringForm}` : stringForm +} + +export const trimDate = stringForm => { + const date = new Date(stringForm) + if (!date) return '--.--.----, --:--:--' + return `${zeros(date.getDate())}.${zeros(date.getMonth() + 1)}.${date.getFullYear()}, ${zeros(date.getHours())}:${zeros(date.getMinutes())}:${zeros(date.getSeconds())}` +}