Browse Source

Merge pull request #2207 from doccano/enhancement/1804

Allow project members to create label types
pull/2208/head
Hiroki Nakayama 1 year ago
committed by GitHub
parent
commit
d5ee4bf1cd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 285 additions and 99 deletions
  1. 7
      backend/api/tests/utils.py
  2. 68
      backend/label_types/tests/test_views.py
  3. 17
      backend/label_types/views.py
  4. 36
      backend/projects/migrations/0008_project_allow_member_to_create_label_type_and_more.py
  5. 1
      backend/projects/models.py
  6. 1
      backend/projects/serializers.py
  7. 35
      frontend/components/label/ActionMenu.vue
  8. 17
      frontend/components/label/LabelList.vue
  9. 8
      frontend/components/layout/TheSideBar.vue
  10. 30
      frontend/components/project/FormUpdate.vue
  11. 37
      frontend/domain/models/project/project.ts
  12. 3
      frontend/layouts/project.vue
  13. 10
      frontend/middleware/isProjectAdmin.ts
  14. 13
      frontend/middleware/setCurrentProject.ts
  15. 3
      frontend/pages/projects/_id/comments/index.vue
  16. 2
      frontend/pages/projects/_id/dataset/_example_id/edit.vue
  17. 2
      frontend/pages/projects/_id/dataset/export.vue
  18. 2
      frontend/pages/projects/_id/dataset/import.vue
  19. 2
      frontend/pages/projects/_id/guideline/index.vue
  20. 2
      frontend/pages/projects/_id/labels/_label_id/edit.vue
  21. 2
      frontend/pages/projects/_id/labels/import.vue
  22. 35
      frontend/pages/projects/_id/labels/index.vue
  23. 3
      frontend/pages/projects/_id/members/index.vue
  24. 2
      frontend/pages/projects/_id/metrics/index.vue
  25. 3
      frontend/pages/projects/_id/settings/index.vue
  26. 19
      frontend/pages/projects/create.vue
  27. 2
      frontend/repositories/project/apiProjectRepository.ts
  28. 13
      frontend/services/application/project/projectApplicationService.ts
  29. 9
      frontend/store/projects.js

7
backend/api/tests/utils.py

@ -29,8 +29,11 @@ class CRUDMixin(APITestCase):
self.assertEqual(response.status_code, expected)
return response
def assert_delete(self, user=None, expected=status.HTTP_403_FORBIDDEN):
def assert_delete(self, user=None, expected=status.HTTP_403_FORBIDDEN, data=None):
if user:
self.client.force_login(user)
response = self.client.delete(self.url)
if data is None:
data = {}
response = self.client.delete(self.url, data=data)
self.assertEqual(response.status_code, expected)

68
backend/label_types/tests/test_views.py

@ -15,16 +15,18 @@ DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
class TestLabelList(CRUDMixin):
@classmethod
def setUpTestData(cls):
cls.non_member = make_user()
cls.project_a = prepare_project(ProjectType.DOCUMENT_CLASSIFICATION)
cls.label = make_label(cls.project_a.item)
cls.url = reverse(viewname="category_types", args=[cls.project_a.item.id])
def setUp(self):
self.non_member = make_user()
self.project_a = prepare_project(ProjectType.DOCUMENT_CLASSIFICATION)
self.label = make_label(self.project_a.item)
self.url = reverse(viewname="category_types", args=[self.project_a.item.id])
# Ensure that the API does not return the labels of the other project.
cls.project_b = make_project(task="Any", users=["admin"], roles=[settings.ROLE_PROJECT_ADMIN])
make_label(cls.project_b.item)
self.project_b = make_project(task="Any", users=["admin"], roles=[settings.ROLE_PROJECT_ADMIN])
make_label(self.project_b.item)
# for label creation
self.data = {"text": "example"}
def test_returns_labels_to_project_member(self):
for member in self.project_a.members:
@ -38,32 +40,11 @@ class TestLabelList(CRUDMixin):
def test_does_not_return_labels_to_unauthenticated_user(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
class TestLabelSearch(CRUDMixin):
def setUp(self):
self.project = prepare_project(ProjectType.DOCUMENT_CLASSIFICATION)
make_label(self.project.item)
self.url = reverse(viewname="category_types", args=[self.project.item.id])
def test_search(self):
for member in self.project.members:
response = self.assert_fetch(member, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
class TestLabelCreate(CRUDMixin):
@classmethod
def setUpTestData(cls):
cls.non_member = make_user(ProjectType.DOCUMENT_CLASSIFICATION)
cls.project = prepare_project()
cls.url = reverse(viewname="category_types", args=[cls.project.item.id])
cls.data = {"text": "example"}
def test_allows_admin_to_create_label(self):
self.assert_create(self.project.admin, status.HTTP_201_CREATED)
self.assert_create(self.project_a.admin, status.HTTP_201_CREATED)
def test_denies_project_staff_to_create_label(self):
for member in self.project.staffs:
for member in self.project_a.staffs:
self.assert_create(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_create_label(self):
@ -72,6 +53,31 @@ class TestLabelCreate(CRUDMixin):
def test_denies_unauthenticated_user_to_create_label(self):
self.assert_create(expected=status.HTTP_403_FORBIDDEN)
def test_allows_admin_to_bulk_delete_label(self):
self.assert_delete(self.project_a.admin, status.HTTP_204_NO_CONTENT, data={"ids": [self.label.id]})
def test_denies_project_staff_to_bulk_delete_label(self):
member = self.project_a.staffs[0]
self.assert_delete(member, status.HTTP_403_FORBIDDEN, data={"ids": [self.label.id]})
class TestAllowMemberToCreateLabelType(CRUDMixin):
@classmethod
def setUpTestData(cls):
cls.project = prepare_project(ProjectType.DOCUMENT_CLASSIFICATION, allow_member_to_create_label_type=True)
cls.label = make_label(cls.project.item)
cls.url = reverse(viewname="category_types", args=[cls.project.item.id])
cls.data = {"text": "example"}
def test_allows_member_to_create_label_type(self):
for member in self.project.members:
self.data["text"] = member.username
self.assert_create(member, status.HTTP_201_CREATED)
def test_denies_project_staff_to_bulk_delete_label(self):
member = self.project.staffs[0]
self.assert_delete(member, status.HTTP_403_FORBIDDEN, data={"ids": [self.label.id]})
class TestLabelDetailAPI(CRUDMixin):
@classmethod

17
backend/label_types/views.py

@ -2,6 +2,7 @@ import json
import re
from django.db import IntegrityError, transaction
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, status
from rest_framework.exceptions import ParseError
@ -18,7 +19,12 @@ from .serializers import (
RelationTypeSerializer,
SpanTypeSerializer,
)
from projects.permissions import IsProjectAdmin, IsProjectStaffAndReadOnly
from projects.models import Project
from projects.permissions import (
IsProjectAdmin,
IsProjectMember,
IsProjectStaffAndReadOnly,
)
def camel_to_snake(name):
@ -35,7 +41,14 @@ class LabelList(generics.ListCreateAPIView):
filter_backends = [DjangoFilterBackend]
serializer_class = LabelSerializer
pagination_class = None
permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]
def get_permissions(self):
project = get_object_or_404(Project, pk=self.kwargs["project_id"])
if project.allow_member_to_create_label_type and self.request.method == "POST":
self.permission_classes = [IsAuthenticated & IsProjectMember]
else:
self.permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]
return super().get_permissions()
def get_queryset(self):
return self.model.objects.filter(project=self.kwargs["project_id"])

36
backend/projects/migrations/0008_project_allow_member_to_create_label_type_and_more.py

@ -0,0 +1,36 @@
# Generated by Django 4.1.7 on 2023-06-07 04:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("projects", "0007_imagecaptioningproject_alter_project_project_type"),
]
operations = [
migrations.AddField(
model_name="project",
name="allow_member_to_create_label_type",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="project",
name="project_type",
field=models.CharField(
choices=[
("DocumentClassification", "Document Classification"),
("SequenceLabeling", "Sequence Labeling"),
("Seq2seq", "Seq2Seq"),
("IntentDetectionAndSlotFilling", "Intent Detection And Slot Filling"),
("Speech2text", "Speech2Text"),
("ImageClassification", "Image Classification"),
("BoundingBox", "Bounding Box"),
("Segmentation", "Segmentation"),
("ImageCaptioning", "Image Captioning"),
],
max_length=30,
),
),
]

1
backend/projects/models.py

@ -38,6 +38,7 @@ class Project(PolymorphicModel):
random_order = models.BooleanField(default=False)
collaborative_annotation = models.BooleanField(default=False)
single_class_classification = models.BooleanField(default=False)
allow_member_to_create_label_type = models.BooleanField(default=False)
def add_admin(self):
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)

1
backend/projects/serializers.py

@ -71,6 +71,7 @@ class ProjectSerializer(serializers.ModelSerializer):
"author",
"collaborative_annotation",
"single_class_classification",
"allow_member_to_create_label_type",
"is_text_project",
"tags",
]

35
frontend/components/label/ActionMenu.vue

@ -18,25 +18,38 @@ export default Vue.extend({
ActionMenu
},
props: {
addOnly: {
type: Boolean,
default: false
}
},
computed: {
items() {
return [
const items = [
{
title: this.$t('labels.createLabel'),
icon: mdiPencil,
event: 'create'
},
{
title: this.$t('labels.importLabels'),
icon: mdiUpload,
event: 'upload'
},
{
title: this.$t('labels.exportLabels'),
icon: mdiDownload,
event: 'download'
}
]
if (this.addOnly) {
return items
} else {
return items.concat([
{
title: this.$t('labels.importLabels'),
icon: mdiUpload,
event: 'upload'
},
{
title: this.$t('labels.exportLabels'),
icon: mdiDownload,
event: 'download'
}
])
}
}
}
})

17
frontend/components/label/LabelList.vue

@ -64,6 +64,10 @@ export default Vue.extend({
type: Array as PropType<LabelDTO[]>,
default: () => [],
required: true
},
disableEdit: {
type: Boolean,
default: false
}
},
@ -77,12 +81,15 @@ export default Vue.extend({
computed: {
headers() {
return [
{ text: this.$t('generic.name'), value: 'text' },
{ text: this.$t('labels.shortkey'), value: 'suffixKey' },
{ text: this.$t('labels.color'), value: 'backgroundColor' },
{ text: 'Actions', value: 'actions', sortable: false }
const headers = [
{ text: this.$t('generic.name'), value: 'text', sortable: true },
{ text: this.$t('labels.shortkey'), value: 'suffixKey', sortable: true },
{ text: this.$t('labels.color'), value: 'backgroundColor', sortable: true }
]
if (!this.disableEdit) {
headers.push({ text: 'Actions', value: 'actions', sortable: false })
}
return headers
}
}
})

8
frontend/components/layout/TheSideBar.vue

@ -81,13 +81,17 @@ export default {
icon: mdiLabel,
text: this.$t('labels.labels'),
link: 'labels',
isVisible: this.isProjectAdmin && this.project.canDefineLabel
isVisible:
(this.isProjectAdmin || this.project.allowMemberToCreateLabelType) &&
this.project.canDefineLabel
},
{
icon: mdiLabel,
text: 'Relations',
link: 'links',
isVisible: this.isProjectAdmin && this.project.canDefineRelation
isVisible:
(this.isProjectAdmin || this.project.allowMemberToCreateLabelType) &&
this.project.canDefineRelation
},
{
icon: mdiAccount,

30
frontend/components/project/FormUpdate.vue

@ -9,33 +9,31 @@
<tag-list v-model="tags" />
<random-order-field v-model="project.enableRandomOrder" />
<sharing-mode-field v-model="project.enableSharingMode" />
<v-checkbox
v-if="project.canDefineLabel"
v-model="project.allowMemberToCreateLabelType"
label="Allow project members to create label types"
/>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions class="ps-4 pt-0">
<v-btn
v-if="!isEditing"
color="primary"
class="text-capitalize"
@click="isEditing = true"
v-text="`Edit`"
/>
<v-btn v-if="!isEditing" color="primary" class="text-capitalize" @click="isEditing = true">
Edit
</v-btn>
<v-btn
v-show="isEditing"
color="primary"
:disabled="!valid || isUpdating"
class="mr-4 text-capitalize"
@click="save"
v-text="$t('generic.save')"
/>
<v-btn
v-show="isEditing"
:disabled="isUpdating"
class="text-capitalize"
@click="cancel"
v-text="$t('generic.cancel')"
/>
>
{{ $t('generic.save') }}
</v-btn>
<v-btn v-show="isEditing" :disabled="isUpdating" class="text-capitalize" @click="cancel">
{{ $t('generic.cancel') }}
</v-btn>
</v-card-actions>
</v-card>
</template>

37
frontend/domain/models/project/project.ts

@ -33,6 +33,24 @@ export const validateNameMaxLength = (name: string): boolean => {
return name.trim().length <= MAX_PROJECT_NAME_LENGTH
}
export const canDefineCategory = (projectType: ProjectType): boolean => {
return [
DocumentClassification,
IntentDetectionAndSlotFilling,
ImageClassification,
BoundingBox,
Segmentation
].includes(projectType)
}
export const canDefineSpan = (projectType: ProjectType): boolean => {
return [SequenceLabeling, IntentDetectionAndSlotFilling].includes(projectType)
}
export const canDefineLabel = (projectType: ProjectType): boolean => {
return canDefineCategory(projectType) || canDefineSpan(projectType)
}
export class Project {
name: string
description: string
@ -50,6 +68,7 @@ export class Project {
readonly enableGraphemeMode: boolean,
readonly useRelation: boolean,
readonly tags: TagItem[],
readonly allowMemberToCreateLabelType: boolean = false,
readonly users: number[] = [],
readonly createdAt: string = '',
readonly updatedAt: string = '',
@ -85,7 +104,8 @@ export class Project {
allowOverlappingSpans: boolean,
enableGraphemeMode: boolean,
useRelation: boolean,
tags: TagItem[]
tags: TagItem[],
allowMemberToCreateLabelType: boolean
) {
return new Project(
id,
@ -99,26 +119,21 @@ export class Project {
allowOverlappingSpans,
enableGraphemeMode,
useRelation,
tags
tags,
allowMemberToCreateLabelType
)
}
get canDefineLabel(): boolean {
return this.canDefineCategory || this.canDefineSpan
return canDefineLabel(this.projectType)
}
get canDefineCategory(): boolean {
return [
DocumentClassification,
IntentDetectionAndSlotFilling,
ImageClassification,
BoundingBox,
Segmentation
].includes(this.projectType)
return canDefineCategory(this.projectType)
}
get canDefineSpan(): boolean {
return [SequenceLabeling, IntentDetectionAndSlotFilling].includes(this.projectType)
return canDefineSpan(this.projectType)
}
get canDefineRelation(): boolean {

3
frontend/layouts/project.vue

@ -32,7 +32,8 @@ export default {
TheSideBar,
TheHeader
},
middleware: ['check-auth', 'auth', 'check-admin'],
middleware: ['check-auth', 'auth', 'setCurrentProject'],
data() {
return {

10
frontend/middleware/isProjectAdmin.ts

@ -0,0 +1,10 @@
import { NuxtAppOptions } from '@nuxt/types'
import _ from 'lodash'
export default _.debounce(async ({ app, route, redirect }: NuxtAppOptions) => {
const member = await app.$repositories.member.fetchMyRole(route.params.id)
if (!member.isProjectAdmin) {
return redirect(app.localePath('/projects/' + route.params.id))
}
}, 1000)

13
frontend/middleware/setCurrentProject.ts

@ -0,0 +1,13 @@
import { NuxtAppOptions } from '@nuxt/types'
export default async ({ app, route, redirect }: NuxtAppOptions) => {
const project = app.store.getters['projects/currentProject']
const isNotSet = Object.keys(project).length === 0 && project.constructor === Object
if (isNotSet || project.id !== route.params.id) {
try {
await app.store.dispatch('projects/setCurrentProject', route.params.id)
} catch (e) {
redirect('/projects')
}
}
}

3
frontend/pages/projects/_id/comments/index.vue

@ -39,8 +39,11 @@ export default Vue.extend({
CommentList,
FormDelete
},
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

2
frontend/pages/projects/_id/dataset/_example_id/edit.vue

@ -27,6 +27,8 @@ import { ExampleDTO } from '~/services/application/example/exampleData'
export default Vue.extend({
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params, app }) {
if (/^\d+$/.test(params.id) && /^\d+$/.test(params.example_id)) {
return app.$services.project.findById(params.id).then((res: Project) => {

2
frontend/pages/projects/_id/dataset/export.vue

@ -44,6 +44,8 @@ import { Format } from '~/domain/models/download/format'
export default Vue.extend({
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

2
frontend/pages/projects/_id/dataset/import.vue

@ -105,6 +105,8 @@ export default {
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

2
frontend/pages/projects/_id/guideline/index.vue

@ -24,6 +24,8 @@ export default {
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

2
frontend/pages/projects/_id/labels/_label_id/edit.vue

@ -19,6 +19,8 @@ export default Vue.extend({
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params, query, app }) {
if (!['category', 'span', 'relation'].includes(query.type as string)) {
return false

2
frontend/pages/projects/_id/labels/import.vue

@ -14,6 +14,8 @@ export default Vue.extend({
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params, query, app }) {
if (!['category', 'span', 'relation'].includes(query.type as string)) {
return false

35
frontend/pages/projects/_id/labels/index.vue

@ -12,11 +12,13 @@
</v-tabs>
<v-card-title>
<action-menu
:add-only="canOnlyAdd"
@create="$router.push('labels/add?type=' + labelType)"
@upload="$router.push('labels/import?type=' + labelType)"
@download="download"
/>
<v-btn
v-if="!canOnlyAdd"
class="text-capitalize ms-2"
:disabled="!canDelete"
outlined
@ -28,7 +30,13 @@
<form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" />
</v-dialog>
</v-card-title>
<label-list v-model="selected" :items="items" :is-loading="isLoading" @edit="editItem" />
<label-list
v-model="selected"
:items="items"
:is-loading="isLoading"
:disable-edit="canOnlyAdd"
@edit="editItem"
/>
</v-card>
</template>
@ -39,6 +47,7 @@ import FormDelete from '@/components/label/FormDelete.vue'
import LabelList from '@/components/label/LabelList.vue'
import { Project } from '~/domain/models/project/project'
import { LabelDTO } from '~/services/application/label/labelData'
import { MemberItem } from '~/domain/models/member/member'
export default Vue.extend({
components: {
@ -46,12 +55,21 @@ export default Vue.extend({
FormDelete,
LabelList
},
layout: 'project',
validate({ params, app }) {
if (/^\d+$/.test(params.id)) {
return app.$services.project.findById(params.id).then((res: Project) => {
return res.canDefineLabel
return app.$services.project.findById(params.id).then((project: Project) => {
if (!project.canDefineLabel) {
return false
}
return app.$repositories.member.fetchMyRole(params.id).then((member: MemberItem) => {
if (member.isProjectAdmin) {
return true
}
return project.allowMemberToCreateLabelType
})
})
}
return false
@ -64,11 +82,19 @@ export default Vue.extend({
selected: [] as LabelDTO[],
isLoading: false,
tab: 0,
project: {} as Project
project: {} as Project,
member: {} as MemberItem
}
},
computed: {
canOnlyAdd(): boolean {
if (this.member.isProjectAdmin) {
return false
}
return this.project.allowMemberToCreateLabelType
},
canDelete(): boolean {
return this.selected.length > 0
},
@ -129,6 +155,7 @@ export default Vue.extend({
async created() {
this.project = await this.$services.project.findById(this.projectId)
this.member = await this.$repositories.member.fetchMyRole(this.projectId)
await this.list()
},

3
frontend/pages/projects/_id/members/index.vue

@ -41,8 +41,11 @@ export default Vue.extend({
FormCreate,
FormDelete
},
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

2
frontend/pages/projects/_id/metrics/index.vue

@ -39,6 +39,8 @@ export default {
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

3
frontend/pages/projects/_id/settings/index.vue

@ -28,8 +28,11 @@ export default Vue.extend({
ConfigList,
FormUpdate
},
layout: 'project',
middleware: ['isProjectAdmin'],
validate({ params }) {
return /^\d+$/.test(params.id)
},

19
frontend/pages/projects/create.vue

@ -12,6 +12,11 @@
v-model="editedItem.exclusiveCategories"
:label="$t('overview.allowSingleLabel')"
/>
<v-checkbox
v-if="_canDefineLabel"
v-model="editedItem.allowMemberToCreateLabelType"
label="Allow project members to create label types"
/>
<template v-if="isSequenceLabelingProject">
<v-checkbox v-model="editedItem.allowOverlappingSpans" label="Allow overlapping spans" />
<v-img
@ -54,8 +59,9 @@
style="text-transform: none"
outlined
@click="create"
v-text="$t('generic.create')"
/>
>
{{ $t('generic.create') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
@ -71,7 +77,8 @@ import TagList from '~/components/project/TagList.vue'
import {
DocumentClassification,
ImageClassification,
SequenceLabeling
SequenceLabeling,
canDefineLabel
} from '~/domain/models/project/project'
const initializeProject = () => {
@ -86,7 +93,8 @@ const initializeProject = () => {
enableGraphemeMode: false,
useRelation: false,
tags: [] as string[],
guideline: ''
guideline: '',
allowMemberToCreateLabelType: false
}
}
@ -117,6 +125,9 @@ export default Vue.extend({
},
isSequenceLabelingProject(): boolean {
return this.editedItem.projectType === SequenceLabeling
},
_canDefineLabel(): boolean {
return canDefineLabel(this.editedItem.projectType as any)
}
},

2
frontend/repositories/project/apiProjectRepository.ts

@ -38,6 +38,7 @@ function toModel(item: { [key: string]: any }): Project {
item.grapheme_mode,
item.use_relation,
item.tags.map((tag: { [key: string]: any }) => new TagItem(tag.id, tag.text, tag.project)),
item.allow_member_to_create_label_type,
item.users,
item.created_at,
item.updated_at,
@ -60,6 +61,7 @@ function toPayload(item: Project): { [key: string]: any } {
grapheme_mode: item.enableGraphemeMode,
use_relation: item.useRelation,
tags: item.tags,
allow_member_to_create_label_type: item.allowMemberToCreateLabelType,
resourcetype: item.resourceType
}
}

13
frontend/services/application/project/projectApplicationService.ts

@ -15,6 +15,7 @@ type ProjectFields = {
allowOverlappingSpans: boolean
enableGraphemeMode: boolean
useRelation: boolean
allowMemberToCreateLabelType: boolean
}
export interface SearchQueryData {
@ -52,7 +53,8 @@ export class ProjectApplicationService {
enableGraphemeMode,
useRelation,
tags,
guideline = ''
guideline = '',
allowMemberToCreateLabelType = false
}: ProjectFields): Promise<Project> {
const project = Project.create(
0,
@ -66,7 +68,8 @@ export class ProjectApplicationService {
allowOverlappingSpans,
enableGraphemeMode,
useRelation,
tags.map((tag) => TagItem.create(tag))
tags.map((tag) => TagItem.create(tag)),
allowMemberToCreateLabelType
)
try {
return await this.repository.create(project)
@ -87,7 +90,8 @@ export class ProjectApplicationService {
allowOverlappingSpans,
enableGraphemeMode,
useRelation,
guideline = ''
guideline = '',
allowMemberToCreateLabelType
}: Omit<ProjectFields, 'tags'>
): Promise<void> {
const project = Project.create(
@ -102,7 +106,8 @@ export class ProjectApplicationService {
allowOverlappingSpans,
enableGraphemeMode,
useRelation,
[]
[],
allowMemberToCreateLabelType
)
try {

9
frontend/store/projects.js

@ -6,12 +6,9 @@ export const getters = {
currentProject(state) {
return state.current
},
getCurrentUserRole(state) {
return state.current.current_users_role || {}
},
canViewApproveButton(state) {
const role = state.current.current_users_role
return role && !role.is_annotator
project(state) {
return state.current
}
}

Loading…
Cancel
Save