mirror of https://github.com/doccano/doccano.git
Browse Source
Merge pull request #2261 from doccano/enhancement/assignment
Merge pull request #2261 from doccano/enhancement/assignment
[Enhancement] Assign examples to memberspull/2276/head
Hiroki Nakayama
1 year ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1001 additions and 119 deletions
Split View
Diff Options
-
0backend/examples/assignment/__init__.py
-
82backend/examples/assignment/strategies.py
-
33backend/examples/assignment/usecase.py
-
20backend/examples/assignment/workload.py
-
45backend/examples/migrations/0008_assignment.py
-
24backend/examples/models.py
-
23backend/examples/serializers.py
-
117backend/examples/tests/test_assignment.py
-
102backend/examples/tests/test_example.py
-
33backend/examples/tests/test_usecase.py
-
4backend/examples/tests/utils.py
-
10backend/examples/urls.py
-
88backend/examples/views/assignment.py
-
18backend/examples/views/example.py
-
3backend/projects/models.py
-
14frontend/components/example/ActionMenu.vue
-
81frontend/components/example/AudioList.vue
-
81frontend/components/example/DocumentList.vue
-
138frontend/components/example/FormAssignment.vue
-
19frontend/components/example/FormResetAssignment.vue
-
81frontend/components/example/ImageList.vue
-
9frontend/domain/models/example/example.ts
-
56frontend/pages/projects/_id/dataset/index.vue
-
4frontend/plugins/repositories.ts
-
28frontend/repositories/example/apiAssignmentRepository.ts
-
3frontend/repositories/example/apiDocumentRepository.ts
-
4frontend/services/application/example/exampleData.ts
@ -0,0 +1,82 @@ |
|||
import abc |
|||
import dataclasses |
|||
import enum |
|||
import random |
|||
from typing import List |
|||
|
|||
import numpy as np |
|||
|
|||
|
|||
@dataclasses.dataclass |
|||
class Assignment: |
|||
user: int |
|||
example: int |
|||
|
|||
|
|||
class StrategyName(enum.Enum): |
|||
weighted_sequential = enum.auto() |
|||
weighted_random = enum.auto() |
|||
sampling_without_replacement = enum.auto() |
|||
|
|||
|
|||
def create_assignment_strategy(strategy_name: StrategyName, dataset_size: int, weights: List[int]) -> "BaseStrategy": |
|||
if strategy_name == StrategyName.weighted_sequential: |
|||
return WeightedSequentialStrategy(dataset_size, weights) |
|||
elif strategy_name == StrategyName.weighted_random: |
|||
return WeightedRandomStrategy(dataset_size, weights) |
|||
elif strategy_name == StrategyName.sampling_without_replacement: |
|||
return SamplingWithoutReplacementStrategy(dataset_size, weights) |
|||
else: |
|||
raise ValueError(f"Unknown strategy name: {strategy_name}") |
|||
|
|||
|
|||
class BaseStrategy(abc.ABC): |
|||
@abc.abstractmethod |
|||
def assign(self) -> List[Assignment]: |
|||
... |
|||
|
|||
|
|||
class WeightedSequentialStrategy(BaseStrategy): |
|||
def __init__(self, dataset_size: int, weights: List[int]): |
|||
if sum(weights) != 100: |
|||
raise ValueError("Sum of weights must be 100") |
|||
self.dataset_size = dataset_size |
|||
self.weights = weights |
|||
|
|||
def assign(self) -> List[Assignment]: |
|||
assignments = [] |
|||
cumsum = np.cumsum([0] + self.weights) |
|||
ratio = np.round(cumsum / 100 * self.dataset_size).astype(int) |
|||
for user, (start, end) in enumerate(zip(ratio, ratio[1:])): # Todo: use itertools.pairwise |
|||
assignments.extend([Assignment(user=user, example=example) for example in range(start, end)]) |
|||
return assignments |
|||
|
|||
|
|||
class WeightedRandomStrategy(BaseStrategy): |
|||
def __init__(self, dataset_size: int, weights: List[int]): |
|||
if sum(weights) != 100: |
|||
raise ValueError("Sum of weights must be 100") |
|||
self.dataset_size = dataset_size |
|||
self.weights = weights |
|||
|
|||
def assign(self) -> List[Assignment]: |
|||
proba = np.array(self.weights) / 100 |
|||
assignees = np.random.choice(range(len(self.weights)), size=self.dataset_size, p=proba) |
|||
return [Assignment(user=user, example=example) for example, user in enumerate(assignees)] |
|||
|
|||
|
|||
class SamplingWithoutReplacementStrategy(BaseStrategy): |
|||
def __init__(self, dataset_size: int, weights: List[int]): |
|||
if not (0 <= sum(weights) <= 100 * len(weights)): |
|||
raise ValueError("Sum of weights must be between 0 and 100 x number of members") |
|||
self.dataset_size = dataset_size |
|||
self.weights = weights |
|||
|
|||
def assign(self) -> List[Assignment]: |
|||
assignments = [] |
|||
proba = np.array(self.weights) / 100 |
|||
for user, p in enumerate(proba): |
|||
count = int(self.dataset_size * p) |
|||
examples = random.sample(range(self.dataset_size), count) |
|||
assignments.extend([Assignment(user=user, example=example) for example in examples]) |
|||
return assignments |
@ -0,0 +1,33 @@ |
|||
from typing import List |
|||
|
|||
from django.shortcuts import get_object_or_404 |
|||
|
|||
from examples.assignment.strategies import StrategyName, create_assignment_strategy |
|||
from examples.models import Assignment, Example |
|||
from projects.models import Member, Project |
|||
|
|||
|
|||
def bulk_assign(project_id: int, strategy_name: StrategyName, member_ids: List[int], weights: List[int]) -> None: |
|||
project = get_object_or_404(Project, pk=project_id) |
|||
members = Member.objects.filter(project=project, pk__in=member_ids) |
|||
if len(members) != len(member_ids): |
|||
raise ValueError("Invalid member ids") |
|||
# Sort members by member_ids |
|||
members = sorted(members, key=lambda m: member_ids.index(m.id)) |
|||
index_to_user = {i: member.user for i, member in enumerate(members)} |
|||
|
|||
unassigned_examples = Example.objects.filter(project=project, assignments__isnull=True) |
|||
index_to_example = {i: example for i, example in enumerate(unassigned_examples)} |
|||
dataset_size = unassigned_examples.count() |
|||
|
|||
strategy = create_assignment_strategy(strategy_name, dataset_size, weights) |
|||
assignments = strategy.assign() |
|||
assignments = [ |
|||
Assignment( |
|||
project=project, |
|||
example=index_to_example[assignment.example], |
|||
assignee=index_to_user[assignment.user], |
|||
) |
|||
for assignment in assignments |
|||
] |
|||
Assignment.objects.bulk_create(assignments) |
@ -0,0 +1,20 @@ |
|||
from typing import List |
|||
|
|||
from pydantic import BaseModel, NonNegativeInt |
|||
|
|||
|
|||
class Workload(BaseModel): |
|||
weight: NonNegativeInt |
|||
member_id: int |
|||
|
|||
|
|||
class WorkloadAllocation(BaseModel): |
|||
workloads: List[Workload] |
|||
|
|||
@property |
|||
def member_ids(self) -> List[int]: |
|||
return [w.member_id for w in self.workloads] |
|||
|
|||
@property |
|||
def weights(self) -> List[int]: |
|||
return [w.weight for w in self.workloads] |
@ -0,0 +1,45 @@ |
|||
# Generated by Django 4.1.10 on 2023-07-24 05:39 |
|||
|
|||
from django.conf import settings |
|||
from django.db import migrations, models |
|||
import django.db.models.deletion |
|||
import uuid |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
("projects", "0008_project_allow_member_to_create_label_type_and_more"), |
|||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
|||
("examples", "0007_example_score"), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.CreateModel( |
|||
name="Assignment", |
|||
fields=[ |
|||
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |
|||
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), |
|||
("updated_at", models.DateTimeField(auto_now=True)), |
|||
( |
|||
"assignee", |
|||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), |
|||
), |
|||
( |
|||
"example", |
|||
models.ForeignKey( |
|||
on_delete=django.db.models.deletion.CASCADE, related_name="assignments", to="examples.example" |
|||
), |
|||
), |
|||
( |
|||
"project", |
|||
models.ForeignKey( |
|||
on_delete=django.db.models.deletion.CASCADE, related_name="assignments", to="projects.project" |
|||
), |
|||
), |
|||
], |
|||
options={ |
|||
"unique_together": {("example", "assignee")}, |
|||
}, |
|||
), |
|||
] |
@ -0,0 +1,117 @@ |
|||
from rest_framework import status |
|||
from rest_framework.reverse import reverse |
|||
|
|||
from .utils import make_assignment, make_doc |
|||
from api.tests.utils import CRUDMixin |
|||
from examples.models import Assignment |
|||
from projects.models import Member |
|||
from projects.tests.utils import prepare_project |
|||
from users.tests.utils import make_user |
|||
|
|||
|
|||
class TestAssignmentList(CRUDMixin): |
|||
def setUp(self): |
|||
self.project = prepare_project() |
|||
self.non_member = make_user() |
|||
self.example = make_doc(self.project.item) |
|||
make_assignment(self.project.item, self.example, self.project.admin) |
|||
self.data = {"example": self.example.id, "assignee": self.project.staffs[0].id} |
|||
self.url = reverse(viewname="assignment_list", args=[self.project.item.id]) |
|||
|
|||
def test_allow_project_member_to_list_assignments(self): |
|||
for member in self.project.members: |
|||
self.assert_fetch(member, status.HTTP_200_OK) |
|||
|
|||
def test_denies_non_project_member_to_list_assignments(self): |
|||
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_unauthenticated_user_to_list_assignments(self): |
|||
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_allows_project_admin_to_assign(self): |
|||
response = self.assert_create(self.project.admin, status.HTTP_201_CREATED) |
|||
self.assertEqual(response.data["example"], self.data["example"]) |
|||
self.assertEqual(response.data["assignee"], self.data["assignee"]) |
|||
|
|||
def test_denies_non_admin_to_assign(self): |
|||
for member in self.project.staffs: |
|||
self.assert_create(member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_non_project_member_to_assign(self): |
|||
self.assert_create(self.non_member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_unauthenticated_user_to_assign(self): |
|||
self.assert_create(expected=status.HTTP_403_FORBIDDEN) |
|||
|
|||
|
|||
class TestAssignmentDetail(CRUDMixin): |
|||
def setUp(self): |
|||
self.project = prepare_project() |
|||
self.non_member = make_user() |
|||
example = make_doc(self.project.item) |
|||
assignment = make_assignment(self.project.item, example, self.project.admin) |
|||
self.data = {"assignee": self.project.staffs[0].id} |
|||
self.url = reverse(viewname="assignment_detail", args=[self.project.item.id, assignment.id]) |
|||
|
|||
def test_allows_project_member_to_get_assignment(self): |
|||
for member in self.project.members: |
|||
self.assert_fetch(member, status.HTTP_200_OK) |
|||
|
|||
def test_denies_non_project_member_to_get_assignment(self): |
|||
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_unauthenticated_user_to_get_assignment(self): |
|||
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_allows_project_admin_to_reassign(self): |
|||
response = self.assert_update(self.project.admin, status.HTTP_200_OK) |
|||
self.assertEqual(response.data["assignee"], self.data["assignee"]) |
|||
|
|||
def test_denies_non_admin_to_reassign(self): |
|||
for member in self.project.staffs: |
|||
self.assert_update(member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_non_project_member_to_reassign(self): |
|||
self.assert_update(self.non_member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_unauthenticated_user_to_reassign(self): |
|||
self.assert_update(expected=status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_allows_project_admin_to_unassign(self): |
|||
self.assert_delete(self.project.admin, status.HTTP_204_NO_CONTENT) |
|||
|
|||
def test_denies_non_admin_to_unassign(self): |
|||
for member in self.project.staffs: |
|||
self.assert_delete(member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_non_project_member_to_unassign(self): |
|||
self.assert_delete(self.non_member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_unauthenticated_user_to_unassign(self): |
|||
self.assert_delete(expected=status.HTTP_403_FORBIDDEN) |
|||
|
|||
|
|||
class TestAssignmentBulk(CRUDMixin): |
|||
def setUp(self): |
|||
self.project = prepare_project() |
|||
self.non_member = make_user() |
|||
self.example = make_doc(self.project.item) |
|||
members = Member.objects.filter(project=self.project.item) |
|||
workloads = [{"member_id": member.id, "weight": 100} for member in members] |
|||
self.data = {"strategy_name": "sampling_without_replacement", "workloads": workloads} |
|||
self.url = reverse(viewname="bulk_assignment", args=[self.project.item.id]) |
|||
|
|||
def test_denies_non_admin_to_bulk_assign(self): |
|||
for member in self.project.staffs: |
|||
self.assert_create(member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_non_project_member_to_bulk_assign(self): |
|||
self.assert_create(self.non_member, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_denies_unauthenticated_user_to_bulk_assign(self): |
|||
self.assert_create(expected=status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_allows_project_admin_to_bulk_assign(self): |
|||
self.assert_create(self.project.admin, status.HTTP_201_CREATED) |
|||
expected = self.project.item.examples.count() * len(self.project.members) |
|||
self.assertEqual(Assignment.objects.count(), expected) |
@ -0,0 +1,33 @@ |
|||
from django.test import TestCase |
|||
from model_mommy import mommy |
|||
|
|||
from examples.assignment.usecase import StrategyName, bulk_assign |
|||
from projects.models import Member, ProjectType |
|||
from projects.tests.utils import prepare_project |
|||
|
|||
|
|||
class TestBulkAssignment(TestCase): |
|||
def setUp(self): |
|||
self.project = prepare_project(ProjectType.SEQUENCE_LABELING) |
|||
self.member_ids = list(Member.objects.values_list("id", flat=True)) |
|||
self.example = mommy.make("Example", project=self.project.item) |
|||
|
|||
def test_raise_error_if_weights_is_invalid(self): |
|||
with self.assertRaises(ValueError): |
|||
bulk_assign( |
|||
self.project.item.id, StrategyName.weighted_sequential, self.member_ids, [0] * len(self.member_ids) |
|||
) |
|||
|
|||
def test_raise_error_if_passing_wrong_member_ids(self): |
|||
with self.assertRaises(ValueError): |
|||
bulk_assign( |
|||
self.project.item.id, |
|||
StrategyName.weighted_sequential, |
|||
self.member_ids + [100], |
|||
[0] * len(self.member_ids), |
|||
) |
|||
|
|||
def test_assign_examples(self): |
|||
bulk_assign(self.project.item.id, StrategyName.weighted_sequential, self.member_ids, [100, 0, 0]) |
|||
self.assertEqual(self.example.assignments.count(), 1) |
|||
self.assertEqual(self.example.assignments.first().assignee, self.project.admin) |
@ -0,0 +1,88 @@ |
|||
from django.shortcuts import get_object_or_404 |
|||
from django_filters.rest_framework import DjangoFilterBackend |
|||
from pydantic import ValidationError |
|||
from rest_framework import filters, generics, status |
|||
from rest_framework.permissions import IsAuthenticated |
|||
from rest_framework.views import APIView, Response |
|||
|
|||
from examples.assignment.strategies import StrategyName |
|||
from examples.assignment.usecase import bulk_assign |
|||
from examples.assignment.workload import WorkloadAllocation |
|||
from examples.models import Assignment |
|||
from examples.serializers import AssignmentSerializer |
|||
from projects.models import Project |
|||
from projects.permissions import IsProjectAdmin, IsProjectStaffAndReadOnly |
|||
|
|||
|
|||
class AssignmentList(generics.ListCreateAPIView): |
|||
serializer_class = AssignmentSerializer |
|||
permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)] |
|||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter) |
|||
ordering_fields = ("created_at", "updated_at") |
|||
model = Assignment |
|||
|
|||
@property |
|||
def project(self): |
|||
return get_object_or_404(Project, pk=self.kwargs["project_id"]) |
|||
|
|||
def get_queryset(self): |
|||
queryset = self.model.objects.filter(project=self.project, assignee=self.request.user) |
|||
return queryset |
|||
|
|||
def perform_create(self, serializer): |
|||
serializer.save(project=self.project) |
|||
|
|||
|
|||
class AssignmentDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = Assignment.objects.all() |
|||
serializer_class = AssignmentSerializer |
|||
lookup_url_kwarg = "assignment_id" |
|||
permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)] |
|||
|
|||
|
|||
class ResetAssignment(APIView): |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
|
|||
@property |
|||
def project(self): |
|||
return get_object_or_404(Project, pk=self.kwargs["project_id"]) |
|||
|
|||
def delete(self, *args, **kwargs): |
|||
Assignment.objects.filter(project=self.project).delete() |
|||
return Response(status=status.HTTP_204_NO_CONTENT) |
|||
|
|||
|
|||
class BulkAssignment(APIView): |
|||
serializer_class = AssignmentSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
|
|||
def post(self, *args, **kwargs): |
|||
try: |
|||
strategy_name = StrategyName[self.request.data["strategy_name"]] |
|||
except KeyError: |
|||
return Response( |
|||
{"detail": "Invalid strategy name"}, |
|||
status=status.HTTP_400_BAD_REQUEST, |
|||
) |
|||
|
|||
try: |
|||
workload_allocation = WorkloadAllocation(workloads=self.request.data["workloads"]) |
|||
except ValidationError as e: |
|||
return Response( |
|||
{"detail": e.errors()}, |
|||
status=status.HTTP_400_BAD_REQUEST, |
|||
) |
|||
|
|||
try: |
|||
bulk_assign( |
|||
project_id=self.kwargs["project_id"], |
|||
strategy_name=strategy_name, |
|||
member_ids=workload_allocation.member_ids, |
|||
weights=workload_allocation.weights, |
|||
) |
|||
except ValueError as e: |
|||
return Response( |
|||
{"detail": str(e)}, |
|||
status=status.HTTP_400_BAD_REQUEST, |
|||
) |
|||
return Response(status=status.HTTP_201_CREATED) |
@ -0,0 +1,138 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-toolbar color="primary white--text" flat> |
|||
<v-toolbar-title>Assign examples to members</v-toolbar-title> |
|||
</v-toolbar> |
|||
<v-card-text> |
|||
<v-container fluid> |
|||
<v-row> |
|||
<v-card-title class="pb-0 pl-3">Select assignment strategy</v-card-title> |
|||
<v-col cols="12"> |
|||
<v-select |
|||
v-model="selectedStrategy" |
|||
:items="strategies" |
|||
item-text="displayName" |
|||
item-value="value" |
|||
outlined |
|||
dense |
|||
hide-details |
|||
></v-select> |
|||
{{ strategies.find((strategy) => strategy.value === selectedStrategy)?.description }} |
|||
The project managers have access to all examples, regardless of whether they are |
|||
assigned or not. |
|||
</v-col> |
|||
</v-row> |
|||
<v-row> |
|||
<v-card-title class="pb-0 pl-3">Allocate weights</v-card-title> |
|||
<v-col v-for="(member, i) in members" :key="member.id" cols="12" class="pt-0 pb-0"> |
|||
<v-subheader class="pl-0">{{ member.username }}</v-subheader> |
|||
<v-slider v-model="workloadAllocation[i]" :max="100" class="align-center"> |
|||
<template #append> |
|||
<v-text-field |
|||
v-model="workloadAllocation[i]" |
|||
class="mt-0 pt-0" |
|||
type="number" |
|||
style="width: 60px" |
|||
></v-text-field> |
|||
</template> |
|||
</v-slider> |
|||
</v-col> |
|||
</v-row> |
|||
</v-container> |
|||
</v-card-text> |
|||
<v-card-actions> |
|||
<v-spacer /> |
|||
<v-btn class="text-capitalize" text color="primary" data-test="cancel-button" @click="cancel"> |
|||
Cancel |
|||
</v-btn> |
|||
<v-btn |
|||
class="text-none" |
|||
text |
|||
:disabled="!validateWeight || isWaiting" |
|||
data-test="delete-button" |
|||
@click="agree" |
|||
> |
|||
Assign |
|||
</v-btn> |
|||
</v-card-actions> |
|||
<v-overlay :value="isWaiting"> |
|||
<v-progress-circular indeterminate size="64" /> |
|||
</v-overlay> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import { MemberItem } from '~/domain/models/member/member' |
|||
|
|||
export default Vue.extend({ |
|||
data() { |
|||
return { |
|||
members: [] as MemberItem[], |
|||
workloadAllocation: [] as number[], |
|||
selectedStrategy: 'weighted_sequential', |
|||
isWaiting: false |
|||
} |
|||
}, |
|||
|
|||
async fetch() { |
|||
this.members = await this.$repositories.member.list(this.projectId) |
|||
this.workloadAllocation = this.members.map(() => Math.round(100 / this.members.length)) |
|||
}, |
|||
|
|||
computed: { |
|||
projectId() { |
|||
return this.$route.params.id |
|||
}, |
|||
|
|||
strategies() { |
|||
return [ |
|||
{ |
|||
displayName: 'Weighted sequential', |
|||
value: 'weighted_sequential', |
|||
description: |
|||
'Assign examples to members in order of their workload. The total weight must equal 100.' |
|||
}, |
|||
{ |
|||
displayName: 'Weighted random', |
|||
value: 'weighted_random', |
|||
description: |
|||
'Assign examples to members randomly based on their workload. The total weight must equal 100.' |
|||
}, |
|||
{ |
|||
displayName: 'Sampling without replacement', |
|||
value: 'sampling_without_replacement', |
|||
description: 'Assign examples to members randomly without replacement.' |
|||
} |
|||
] |
|||
}, |
|||
|
|||
validateWeight(): boolean { |
|||
if (this.selectedStrategy === 'sampling_without_replacement') { |
|||
return true |
|||
} else { |
|||
return this.workloadAllocation.reduce((acc, cur) => acc + cur, 0) === 100 |
|||
} |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
async agree() { |
|||
this.isWaiting = true |
|||
const workloads = this.workloadAllocation.map((weight, i) => ({ |
|||
weight, |
|||
member_id: this.members[i].id |
|||
})) |
|||
await this.$repositories.assignment.bulkAssign(this.projectId, { |
|||
strategy_name: this.selectedStrategy, |
|||
workloads |
|||
}) |
|||
this.isWaiting = false |
|||
this.$emit('assigned') |
|||
}, |
|||
cancel() { |
|||
this.$emit('cancel') |
|||
} |
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,19 @@ |
|||
<template> |
|||
<confirm-form |
|||
title="Reset assignment" |
|||
message="Are you sure you want to reset all the assignments?" |
|||
@ok="$emit('reset')" |
|||
@cancel="$emit('cancel')" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import ConfirmForm from '@/components/utils/ConfirmForm.vue' |
|||
|
|||
export default Vue.extend({ |
|||
components: { |
|||
ConfirmForm |
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,28 @@ |
|||
import ApiService from '@/services/api.service' |
|||
import { Assignment } from '@/domain/models/example/example' |
|||
|
|||
export class APIAssignmentRepository { |
|||
constructor(private readonly request = ApiService) {} |
|||
|
|||
async assign(projectId: string, exampleId: number, userId: number): Promise<Assignment> { |
|||
const url = `/projects/${projectId}/assignments` |
|||
const payload = { example: exampleId, assignee: userId } |
|||
const response = await this.request.post(url, payload) |
|||
return response.data |
|||
} |
|||
|
|||
async unassign(projectId: string, assignmentId: string): Promise<void> { |
|||
const url = `/projects/${projectId}/assignments/${assignmentId}` |
|||
await this.request.delete(url) |
|||
} |
|||
|
|||
async bulkAssign(projectId: string, workloadAllocation: Object): Promise<void> { |
|||
const url = `/projects/${projectId}/assignments/bulk_assign` |
|||
await this.request.post(url, workloadAllocation) |
|||
} |
|||
|
|||
async reset(projectId: string): Promise<void> { |
|||
const url = `/projects/${projectId}/assignments/reset` |
|||
await this.request.delete(url) |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save