Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement] Separate label type app #1655

Merged
merged 6 commits into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 2 additions & 19 deletions backend/api/admin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
from django.contrib import admin

from .models import (CategoryType, Comment, Example, Project, Seq2seqProject,
SequenceLabelingProject, SpanType, Tag,
TextClassificationProject)


class LabelAdmin(admin.ModelAdmin):
list_display = ('text', 'project', 'text_color', 'background_color')
ordering = ('project',)
search_fields = ('text',)


class CategoryTypeAdmin(LabelAdmin):
pass


class SpanTypeAdmin(LabelAdmin):
pass
from .models import (Comment, Example, Project, Seq2seqProject,
SequenceLabelingProject, Tag, TextClassificationProject)


class ExampleAdmin(admin.ModelAdmin):
Expand All @@ -43,8 +28,6 @@ class CommentAdmin(admin.ModelAdmin):
search_fields = ('user',)


admin.site.register(CategoryType, CategoryTypeAdmin)
admin.site.register(SpanType, SpanTypeAdmin)
admin.site.register(Example, ExampleAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(TextClassificationProject, ProjectAdmin)
Expand Down
5 changes: 3 additions & 2 deletions backend/api/migrations/0018_alter_label_background_color.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Generated by Django 3.2.8 on 2021-11-17 05:56

import api.models
from django.db import migrations, models

from label_types.models import generate_random_hex_color


class Migration(migrations.Migration):

Expand All @@ -14,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='label',
name='background_color',
field=models.CharField(default=api.models.generate_random_hex_color, max_length=7),
field=models.CharField(default=generate_random_hex_color, max_length=7),
),
]
7 changes: 4 additions & 3 deletions backend/api/migrations/0020_auto_20211221_1415.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Generated by Django 3.2.8 on 2021-12-21 14:15

import api.models
from django.db import migrations, models
import django.db.models.deletion

from label_types.models import generate_random_hex_color


class Migration(migrations.Migration):

Expand All @@ -19,7 +20,7 @@ class Migration(migrations.Migration):
('text', models.CharField(db_index=True, max_length=100)),
('prefix_key', models.CharField(blank=True, choices=[('ctrl', 'ctrl'), ('shift', 'shift'), ('ctrl shift', 'ctrl shift')], max_length=10, null=True)),
('suffix_key', models.CharField(blank=True, choices=[('0', '0'), ('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), ('a', 'a'), ('b', 'b'), ('c', 'c'), ('d', 'd'), ('e', 'e'), ('f', 'f'), ('g', 'g'), ('h', 'h'), ('i', 'i'), ('j', 'j'), ('k', 'k'), ('l', 'l'), ('m', 'm'), ('n', 'n'), ('o', 'o'), ('p', 'p'), ('q', 'q'), ('r', 'r'), ('s', 's'), ('t', 't'), ('u', 'u'), ('v', 'v'), ('w', 'w'), ('x', 'x'), ('y', 'y'), ('z', 'z')], max_length=1, null=True)),
('background_color', models.CharField(default=api.models.generate_random_hex_color, max_length=7)),
('background_color', models.CharField(default=generate_random_hex_color, max_length=7)),
('text_color', models.CharField(default='#ffffff', max_length=7)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
Expand All @@ -36,7 +37,7 @@ class Migration(migrations.Migration):
('text', models.CharField(db_index=True, max_length=100)),
('prefix_key', models.CharField(blank=True, choices=[('ctrl', 'ctrl'), ('shift', 'shift'), ('ctrl shift', 'ctrl shift')], max_length=10, null=True)),
('suffix_key', models.CharField(blank=True, choices=[('0', '0'), ('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), ('a', 'a'), ('b', 'b'), ('c', 'c'), ('d', 'd'), ('e', 'e'), ('f', 'f'), ('g', 'g'), ('h', 'h'), ('i', 'i'), ('j', 'j'), ('k', 'k'), ('l', 'l'), ('m', 'm'), ('n', 'n'), ('o', 'o'), ('p', 'p'), ('q', 'q'), ('r', 'r'), ('s', 's'), ('t', 't'), ('u', 'u'), ('v', 'v'), ('w', 'w'), ('x', 'x'), ('y', 'y'), ('z', 'z')], max_length=1, null=True)),
('background_color', models.CharField(default=api.models.generate_random_hex_color, max_length=7)),
('background_color', models.CharField(default=generate_random_hex_color, max_length=7)),
('text_color', models.CharField(default='#ffffff', max_length=7)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
Expand Down
34 changes: 34 additions & 0 deletions backend/api/migrations/0032_auto_20220127_0654.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.11 on 2022-01-27 06:54

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0031_auto_20220127_0032'),
]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(
model_name='categorytype',
name='project',
),
migrations.AlterUniqueTogether(
name='relationtypes',
unique_together=None,
),
migrations.RemoveField(
model_name='relationtypes',
name='project',
),
migrations.RemoveField(
model_name='spantype',
name='project',
),
],
database_operations=[]
)
]
41 changes: 41 additions & 0 deletions backend/api/migrations/0033_auto_20220127_0654.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 3.2.11 on 2022-01-27 06:54

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('labels', '0003_auto_20220127_0654'),
('api', '0032_auto_20220127_0654'),
]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='CategoryType',
),
migrations.DeleteModel(
name='RelationTypes',
),
migrations.DeleteModel(
name='SpanType',
),
],
database_operations=[
migrations.AlterModelTable(
name='CategoryType',
table='label_types_categorytype'
),
migrations.AlterModelTable(
name='RelationTypes',
table='label_types_relationtypes'
),
migrations.AlterModelTable(
name='SpanType',
table='label_types_spanType'
)
]
)
]
96 changes: 0 additions & 96 deletions backend/api/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import abc
import random
import string
import uuid

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from polymorphic.models import PolymorphicModel

Expand Down Expand Up @@ -151,87 +148,6 @@ def can_define_category(self) -> bool:
return True


def generate_random_hex_color():
return f'#{random.randint(0, 0xFFFFFF):06x}'


class Label(models.Model):
text = models.CharField(max_length=100, db_index=True)
prefix_key = models.CharField(
max_length=10,
blank=True,
null=True,
choices=(
('ctrl', 'ctrl'),
('shift', 'shift'),
('ctrl shift', 'ctrl shift')
)
)
suffix_key = models.CharField(
max_length=1,
blank=True,
null=True,
choices=tuple(
(c, c) for c in string.digits + string.ascii_lowercase
)
)
project = models.ForeignKey(
to=Project,
on_delete=models.CASCADE,
# related_name='labels'
)
background_color = models.CharField(max_length=7, default=generate_random_hex_color)
text_color = models.CharField(max_length=7, default='#ffffff')
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.text

@property
def labels(self):
raise NotImplementedError()

def clean(self):
# Don't allow shortcut key not to have a suffix key.
if self.prefix_key and not self.suffix_key:
message = 'Shortcut key may not have a suffix key.'
raise ValidationError(message)

# each shortcut (prefix key + suffix key) can only be assigned to one label
if self.suffix_key or self.prefix_key:
other_labels = self.labels.exclude(id=self.id)
if other_labels.filter(suffix_key=self.suffix_key, prefix_key=self.prefix_key).exists():
message = 'A label with the shortcut already exists in the project.'
raise ValidationError(message)

super().clean()

class Meta:
abstract = True
constraints = [
models.UniqueConstraint(
fields=['project', 'text'],
name='%(app_label)s_%(class)s_is_unique'
)
]
ordering = ['created_at']


class CategoryType(Label):

@property
def labels(self):
return CategoryType.objects.filter(project=self.project)


class SpanType(Label):

@property
def labels(self):
return SpanType.objects.filter(project=self.project)


class Example(models.Model):
objects = ExampleManager()

Expand Down Expand Up @@ -318,15 +234,3 @@ class Tag(models.Model):

def __str__(self):
return self.text


class RelationTypes(models.Model):
color = models.TextField()
name = models.TextField()
project = models.ForeignKey(Project, related_name='relation_types', on_delete=models.CASCADE)

def __str__(self):
return self.name

class Meta:
unique_together = ('color', 'name')
101 changes: 4 additions & 97 deletions backend/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,87 +1,11 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import PolymorphicSerializer

from .models import (CategoryType, Comment, Example, ExampleState,
from .models import (Comment, Example, ExampleState,
ImageClassificationProject,
IntentDetectionAndSlotFillingProject, Label, Project,
RelationTypes, Seq2seqProject, SequenceLabelingProject,
SpanType, Speech2textProject, Tag,
TextClassificationProject)


class LabelSerializer(serializers.ModelSerializer):

def validate(self, attrs):
prefix_key = attrs.get('prefix_key')
suffix_key = attrs.get('suffix_key')

# In the case of user don't set any shortcut key.
if prefix_key is None and suffix_key is None:
return super().validate(attrs)

# Don't allow shortcut key not to have a suffix key.
if prefix_key and not suffix_key:
raise ValidationError('Shortcut key may not have a suffix key.')

# Don't allow to save same shortcut key when prefix_key is null.
try:
context = self.context['request'].parser_context
project_id = context['kwargs']['project_id']
label_id = context['kwargs'].get('label_id')
except (AttributeError, KeyError):
pass # unit tests don't always have the correct context set up
else:
conflicting_labels = self.Meta.model.objects.filter(
suffix_key=suffix_key,
prefix_key=prefix_key,
project=project_id,
)

if label_id is not None:
conflicting_labels = conflicting_labels.exclude(id=label_id)

if conflicting_labels.exists():
raise ValidationError('Duplicate shortcut key.')

return super().validate(attrs)

class Meta:
model = Label
fields = (
'id',
'text',
'prefix_key',
'suffix_key',
'background_color',
'text_color',
)


class CategoryTypeSerializer(LabelSerializer):
class Meta:
model = CategoryType
fields = (
'id',
'text',
'prefix_key',
'suffix_key',
'background_color',
'text_color',
)


class SpanTypeSerializer(LabelSerializer):
class Meta:
model = SpanType
fields = (
'id',
'text',
'prefix_key',
'suffix_key',
'background_color',
'text_color',
)
IntentDetectionAndSlotFillingProject, Project,
Seq2seqProject, SequenceLabelingProject,
Speech2textProject, Tag, TextClassificationProject)


class CommentSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -139,13 +63,6 @@ class Meta:
read_only_fields = ('id', 'example', 'confirmed_by')


class ApproverSerializer(ExampleSerializer):

class Meta:
model = Example
fields = ('id', 'annotation_approver')


class ProjectSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, required=False)

Expand Down Expand Up @@ -224,13 +141,3 @@ class ProjectPolymorphicSerializer(PolymorphicSerializer):
cls.Meta.model: cls for cls in ProjectSerializer.__subclasses__()
}
}


class RelationTypesSerializer(serializers.ModelSerializer):

def validate(self, attrs):
return super().validate(attrs)

class Meta:
model = RelationTypes
fields = ('id', 'color', 'name')