diff --git a/Pipfile b/Pipfile index 9e887c66..bacbd998 100644 --- a/Pipfile +++ b/Pipfile @@ -52,3 +52,4 @@ isort = "isort . -c" wait_for_db = "python manage.py wait_for_db" test = "python manage.py test api.tests" migrate = "python manage.py migrate" +collectstatic = "python manage.py collectstatic --noinput" diff --git a/README.md b/README.md index f9ffeddb..ac5c73e9 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,14 @@ $ docker-compose -f docker-compose.dev.yml up Go to . + +Run Backend API-Tests: + +You can run the API-Tests for the backend with the following command: +```bash +docker exec doccano_backend_1 pipenv run app/manage.py test api +``` + ### Add annotators (optionally) If you want to add annotators/annotation approvers, see [Frequently Asked Questions](./docs/faq.md) diff --git a/app/server/static/components/annotation.pug b/app/server/static/components/annotation.pug index 9101db8c..6206b540 100644 --- a/app/server/static/components/annotation.pug +++ b/app/server/static/components/annotation.pug @@ -106,28 +106,11 @@ div.columns(v-cloak="") ) section.modal-card-body.modal-card-body-footer vue-json-pretty( - v-bind:data="documentMetadata" + v-bind:data="displayDocumentMetadata" v-bind:show-double-quotes="false" v-bind:show-line="false" ) - div.modal(v-bind:class="{ 'is-active': isCommentActive }") - div.modal-background - div.modal-card - header.modal-card-head - p.modal-card-title Document Comment - button.delete( - v-on:click="toggleCommentModal()" - aria-label="close" - ) - section.modal-card-body.modal-card-body-footer - textarea.textarea( - v-model="comment" - v-debounce="syncComment" - type="text" - placeholder="Add document comment here..." - ) - div.columns.is-multiline.is-gapless.is-mobile.is-vertical-center div.column.is-3 progress.progress.is-inline-block( @@ -135,7 +118,7 @@ div.columns(v-cloak="") v-bind:value="achievement" max="100" ) 30% - div.column.is-5 + div.column.is-6 span.ml10 strong {{ total - remaining }} | / @@ -161,13 +144,6 @@ div.columns(v-cloak="") ) span.icon i.fas.fa-box - div.column.is-1.has-text-right - a.button.tooltip.is-tooltip-bottom( - v-on:click="toggleCommentModal()" - v-bind:data-tooltip="'Click to comment on document.'" - ) - span.icon - i.fas.fa-comment div.columns div.column diff --git a/app/server/static/components/annotationMixin.js b/app/server/static/components/annotationMixin.js index 5bb29ea1..d8e41d11 100644 --- a/app/server/static/components/annotationMixin.js +++ b/app/server/static/components/annotationMixin.js @@ -88,53 +88,18 @@ export default { offset: getOffsetFromUrl(window.location.href), picked: 'all', ordering: '', - comments: [], - comment: '', count: 0, prevLimit: 0, paginationPages: 0, paginationPage: 0, singleClassClassification: false, isAnnotationApprover: false, - isCommentActive: false, isMetadataActive: false, isAnnotationGuidelineActive: false, }; }, methods: { - async syncComment(text) { - const docId = this.docs[this.pageNumber].id; - const commentId = this.comments[this.pageNumber].id; - const hasText = text.trim().length > 0; - - if (commentId && !hasText) { - await HTTP.delete(`docs/${docId}/comments/${commentId}`); - const comments = this.comments.slice(); - comments[this.pageNumber] = { text: '', id: null }; - this.comments = comments; - } else if (commentId && hasText) { - await HTTP.patch(`docs/${docId}/comments/${commentId}`, { text }); - const comments = this.comments.slice(); - comments[this.pageNumber].text = text; - this.comments = comments; - } else { - const response = await HTTP.post(`docs/${docId}/comments`, { text }); - const comments = this.comments.slice(); - comments[this.pageNumber] = response.data; - this.comments = comments; - } - }, - - async toggleCommentModal() { - if (this.isCommentActive) { - this.isCommentActive = false; - return; - } - - this.comment = this.comments[this.pageNumber].text; - this.isCommentActive = true; - }, resetScrollbar() { const textbox = this.$refs.textbox; @@ -278,20 +243,9 @@ export default { }); }, - async fetchComments() { - const responses = await Promise.all(this.docs.map(doc => HTTP.get(`docs/${doc.id}/comments`))); - this.comments = responses.map(response => response.data.results[0] || { text: '', id: null }); - }, }, watch: { - pageNumber() { - this.comment = this.comments[this.pageNumber].text; - }, - - docs() { - this.fetchComments(); - }, picked() { this.submit(); @@ -336,7 +290,13 @@ export default { }, compiledMarkdown() { - return marked(this.guideline, { + const documentMetadata = this.documentMetadata; + + const guideline = documentMetadata && documentMetadata.guideline + ? documentMetadata.guideline + : this.guideline; + + return marked(guideline, { sanitize: true, }); }, @@ -354,6 +314,18 @@ export default { : 'Click to approve annotations'; }, + displayDocumentMetadata() { + let documentMetadata = this.documentMetadata; + if (documentMetadata == null) { + return null; + } + + documentMetadata = { ...documentMetadata }; + delete documentMetadata.guideline; + delete documentMetadata.documentSourceUrl; + return documentMetadata; + }, + documentMetadata() { return this.documentMetadataFor(this.pageNumber); }, diff --git a/docs/project_structure.md b/docs/project_structure.md index c7615f97..4690e196 100644 --- a/docs/project_structure.md +++ b/docs/project_structure.md @@ -16,11 +16,11 @@ Consider them: **[app/](https://github.com/doccano/doccano/tree/master/app)** -The `app/api` directory contains backend code. See [below](#Backend). +The `app/` directory contains backend code. See [below](#Backend). **[frontend/](https://github.com/doccano/doccano/tree/master/frontend)** -The `app/api` directory contains frontend code. See [below](#Frontend). +The `frontend/` directory contains frontend code. See [below](#Frontend). **[docker-compose.dev.yml](https://github.com/doccano/doccano/blob/master/docker-compose.dev.yml)** diff --git a/frontend/components/containers/comments/Comment.vue b/frontend/components/containers/comments/Comment.vue new file mode 100644 index 00000000..0424e05d --- /dev/null +++ b/frontend/components/containers/comments/Comment.vue @@ -0,0 +1,99 @@ + + + + + {{ comment.username }} @{{ comment.created_at | dateParse('YYYY-MM-DDTHH:mm:ss') | dateFormat('YYYY-MM-DD HH:mm') }} + + + + mdi-comment-edit-outline + + + Edit Comment + + + + + mdi-delete-outline + + + Delete Comment + + + + {{ comment.text }} + + + + + + Close + + + Update + + + + + + + + diff --git a/frontend/components/containers/comments/CommentSection.vue b/frontend/components/containers/comments/CommentSection.vue new file mode 100644 index 00000000..d8ab1eaa --- /dev/null +++ b/frontend/components/containers/comments/CommentSection.vue @@ -0,0 +1,114 @@ + + + + {{ this.$t('comments.comments') }} + + + + + + + {{ this.$t('comments.send') }} + + + + + + + + diff --git a/frontend/components/containers/documents/DocumentList.vue b/frontend/components/containers/documents/DocumentList.vue index ded182fe..baa2a54c 100644 --- a/frontend/components/containers/documents/DocumentList.vue +++ b/frontend/components/containers/documents/DocumentList.vue @@ -42,6 +42,9 @@ + + {{ item.comment_count }} + - {{ $t('errors.fileCannotUpload') }} + {{ $t('errors.fileCannotUpload') + errorMsg }} {{ $t('dataset.importDataMessage1') }} {{ $t('dataset.importDataMessage2') }} { - this.reset() - this.cancel() + this.errors = [] + const promises = [] + const id = this.$route.params.id + const type = this.selectedFormat.type + this.file.forEach((item) => { + promises.push({ + projectId: id, + format: type, + file: item }) - .catch(() => { + }) + let p = Promise.resolve() + promises.forEach((item) => { + p = p.then(() => this.uploadDocument(item)).catch(() => { + this.errors.push(item.file.name) this.showError = true }) + }) + p.finally(() => { + if (!this.errors.length) { + this.reset() + this.cancel() + } else { + this.errorMsg = this.errors.join(', ') + } + }) } } } diff --git a/frontend/components/organisms/layout/TheSideBar.vue b/frontend/components/organisms/layout/TheSideBar.vue index 9992d080..77d5f247 100644 --- a/frontend/components/organisms/layout/TheSideBar.vue +++ b/frontend/components/organisms/layout/TheSideBar.vue @@ -65,8 +65,10 @@ export default { { icon: 'mdi-database', text: this.$t('dataset.dataset'), link: 'dataset', adminOnly: true }, { icon: 'label', text: this.$t('labels.labels'), link: 'labels', adminOnly: true }, { icon: 'person', text: this.$t('members.members'), link: 'members', adminOnly: true }, + { icon: 'mdi-comment-account-outline', text: 'Comments', link: 'comments', adminOnly: true }, { icon: 'mdi-book-open-outline', text: this.$t('guideline.guideline'), link: 'guideline', adminOnly: true }, - { icon: 'mdi-chart-bar', text: this.$t('statistics.statistics'), link: 'statistics', adminOnly: true } + { icon: 'mdi-chart-bar', text: this.$t('statistics.statistics'), link: 'statistics', adminOnly: true }, + { icon: 'mdi-cog', text: this.$t('settings.title'), link: 'settings', adminOnly: true } ] return items.filter(item => this.isVisible(item)) } diff --git a/frontend/i18n/en/index.js b/frontend/i18n/en/index.js index 0f1ae8e6..41dde7fa 100644 --- a/frontend/i18n/en/index.js +++ b/frontend/i18n/en/index.js @@ -12,8 +12,10 @@ import guideline from './projects/guideline' import projectHome from './projects/home' import labels from './projects/labels' import members from './projects/members' +import comments from './projects/comments' import overview from './projects/overview' import statistics from './projects/statistics' +import settings from './projects/settings' export default { home, @@ -30,6 +32,8 @@ export default { projectHome, labels, members, + comments, overview, - statistics + statistics, + settings } diff --git a/frontend/i18n/en/projects/comments.js b/frontend/i18n/en/projects/comments.js new file mode 100644 index 00000000..4866b087 --- /dev/null +++ b/frontend/i18n/en/projects/comments.js @@ -0,0 +1,10 @@ +export default { + comments: 'Comments', + removeComment: 'Remove Comment', + removePrompt: 'Are you sure you want to remove these comments?', + commentView: 'View', + created_at: 'Created at', + document: 'Document', + send: 'Send', + message: 'Message' +} diff --git a/frontend/i18n/en/projects/dataset.js b/frontend/i18n/en/projects/dataset.js index 2d7959df..b9930dab 100644 --- a/frontend/i18n/en/projects/dataset.js +++ b/frontend/i18n/en/projects/dataset.js @@ -9,7 +9,7 @@ export default { annotate: 'Annotate', importDataTitle: 'Upload Data', importDataMessage1: 'Select a file format', - importDataMessage2: 'Select a file', + importDataMessage2: 'Select file(s)', importDataPlaceholder: 'File input', exportDataTitle: 'Export Data', exportDataMessage: 'Select a file format', diff --git a/frontend/i18n/en/projects/errors.js b/frontend/i18n/en/projects/errors.js index f84d04a9..fcb93846 100644 --- a/frontend/i18n/en/projects/errors.js +++ b/frontend/i18n/en/projects/errors.js @@ -1,5 +1,5 @@ export default { - fileCannotUpload: 'The file could not be uploaded. Maybe invalid format.\n Please check available formats carefully.', + fileCannotUpload: 'The file(s) could not be uploaded. Maybe invalid format.\n Please check available formats and the following file(s): ', labelCannotCreate: 'The label could not be created.\n You cannot use the same label name or shortcut key.', invalidUserOrPass: 'Incorrect username or password, or something went wrong.' } diff --git a/frontend/i18n/en/projects/settings.js b/frontend/i18n/en/projects/settings.js new file mode 100644 index 00000000..966e6918 --- /dev/null +++ b/frontend/i18n/en/projects/settings.js @@ -0,0 +1,3 @@ +export default { + title: 'Settings' +} diff --git a/frontend/i18n/en/rules.js b/frontend/i18n/en/rules.js index e219d293..d9f54c23 100644 --- a/frontend/i18n/en/rules.js +++ b/frontend/i18n/en/rules.js @@ -29,7 +29,7 @@ export default { }, uploadFileRules: { fileRequired: 'File is required', - fileLessThan1MB: 'File size should be less than 1 MB!' + fileLessThan1MB: 'File size should be less than 100 MB!' }, passwordRules: { passwordRequired: 'Password is required', diff --git a/frontend/i18n/index.js b/frontend/i18n/index.js index be51924a..f36dd3fb 100644 --- a/frontend/i18n/index.js +++ b/frontend/i18n/index.js @@ -9,7 +9,7 @@ export default { { name: '中文', code: 'zh', - iso: 'zh-CA', + iso: 'zh-CN', file: 'zh' }, { @@ -17,6 +17,12 @@ export default { code: 'fr', iso: 'fr-CA', file: 'fr' + }, + { + name: 'Deutsch', + code: 'de', + iso: 'de-DE', + file: 'de' } ], lazy: true, diff --git a/frontend/layouts/annotation.vue b/frontend/layouts/annotation.vue index b964b4ec..54abf6c9 100644 --- a/frontend/layouts/annotation.vue +++ b/frontend/layouts/annotation.vue @@ -39,7 +39,6 @@ v-model="filterOption" /> - @@ -62,8 +61,14 @@ + + + + + + + - + + + + + + + + + diff --git a/frontend/pages/projects/_id/settings/index.vue b/frontend/pages/projects/_id/settings/index.vue new file mode 100644 index 00000000..e2f7974b --- /dev/null +++ b/frontend/pages/projects/_id/settings/index.vue @@ -0,0 +1,225 @@ + + + + About project + + + + + + Name + + + + + Edit + + + Save + + + Cancel + + + + + + Description + + + + + Edit + + + Save + + + Cancel + + + + + + Shuffle + + + + + + Collaboration + + + + + + + + + diff --git a/frontend/rules/index.js b/frontend/rules/index.js index 3f5219b3..edaad7e8 100644 --- a/frontend/rules/index.js +++ b/frontend/rules/index.js @@ -57,7 +57,7 @@ export const fileFormatRules = (msg) => { export const uploadFileRules = (msg) => { return [ v => !!v || msg.fileRequired, - v => !v || v.size < 1000000 || msg.fileLessThan1MB + v => !v || v.some(file => file.size < 100000000) || msg.fileLessThan1MB ] } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6fefd772..e2e59391 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1946,6 +1946,16 @@ lodash "^4.17.15" pretty "^2.0.0" +"@vuejs-community/vue-filter-date-format@^1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@vuejs-community/vue-filter-date-format/-/vue-filter-date-format-1.6.3.tgz#10a9b11425f4288f3ebc14c79c7de29322dafec7" + integrity sha512-fUjAAI/1qzJPR6AYoK00TBA6/aRYKTXjLY/rSYsWV4G5nwywKfrpOYOJi5exWyzozohyV6nrzliDlWcl32+IPg== + +"@vuejs-community/vue-filter-date-parse@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@vuejs-community/vue-filter-date-parse/-/vue-filter-date-parse-1.1.6.tgz#8679a990af0f74b24c58d210ff2341e5b884a059" + integrity sha512-OzeiL5wrq+5+sw9hJfcfaIxcG953lpe37Lf0JnRKzVvfBRTHX2t8SIVxtDmgLhSGoGx9B4bGbpv4NaIqeEofXA== + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 3d482f4c..22ac7d7f 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,7 +1,7 @@ server { listen 80; charset utf-8; - + client_max_body_size 100M; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f17ed285 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,16 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@vuejs-community/vue-filter-date-format": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@vuejs-community/vue-filter-date-format/-/vue-filter-date-format-1.6.3.tgz", + "integrity": "sha512-fUjAAI/1qzJPR6AYoK00TBA6/aRYKTXjLY/rSYsWV4G5nwywKfrpOYOJi5exWyzozohyV6nrzliDlWcl32+IPg==" + }, + "@vuejs-community/vue-filter-date-parse": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@vuejs-community/vue-filter-date-parse/-/vue-filter-date-parse-1.1.6.tgz", + "integrity": "sha512-OzeiL5wrq+5+sw9hJfcfaIxcG953lpe37Lf0JnRKzVvfBRTHX2t8SIVxtDmgLhSGoGx9B4bGbpv4NaIqeEofXA==" + } + } +}