Skip to content

Commit

Permalink
Merge branch 'develop' into 'fb-leap-1370/zoom-image-preview'
Browse files Browse the repository at this point in the history
  • Loading branch information
hlomzik committed Dec 17, 2024
2 parents 876b69c + 4dd9e7a commit dfabd62
Show file tree
Hide file tree
Showing 28 changed files with 843 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ jobs:
fail-fast: false
matrix:
python-version:
- '3.9'
- '3.10'

# required for poetry action
# see https://github.com/marketplace/actions/install-poetry-action#running-on-windows
Expand Down
471 changes: 471 additions & 0 deletions docs/source/guide/release_notes.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/source/playground/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ meta_description: Label Studio interactive demo and playground for data labeling
<h2 class="Heading Large">Want to put these templates into practice?</h2>
<div class="playground-cta-button-group">
<a href="https://labelstud.io/guide/install.html" class="Button Secondary">Install Open Source</a>
<a href="https://humansignal.com/free-trial" class="Button">Try Enterprise Cloud</a>
<a href="https://humansignal.com/pricing" class="Button">Compare Versions</a>
</div>
<svg width="67" height="68" viewBox="0 0 67 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="22.4653" y="45.0219" width="22.2281" height="22.2281" fill="#FF7557"/>
Expand Down
9 changes: 7 additions & 2 deletions docs/themes/v2/layout/page.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
</div>
<% } %>
<div class="toc-enterprise-cta">
<span class="toc-enterprise-cta-copy">Unlock more with Enterprise</span>
<%- partial("component/button", { url: "https://humansignal.com/contact-sales", label: "Get a Demo", theme: "Tertiary" }) %>
<% if(isEnterpriseTheme) { %>
<span class="toc-enterprise-cta-copy">Designed for teams of all sizes</span>
<%- partial("component/button", { url: "https://humansignal.com/contact-sales", label: "Contact Sales", theme: "Tertiary" }) %>
<% } else { %>
<span class="toc-enterprise-cta-copy">Designed for teams of all sizes</span>
<%- partial("component/button", { url: "https://humansignal.com/pricing", label: "Compare Versions", theme: "Tertiary" }) %>
<% } %>
</div>
</div>
</div>
2 changes: 1 addition & 1 deletion docs/themes/v2/source/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ ul {

.toc-enterprise-cta {
margin-top: 2em;
padding: 1.5em 2em;
padding: 1.5em 1em;
color: var(--color-gray-900);
background: url("/images/design/gradient.svg");
background-size: cover;
Expand Down
6 changes: 6 additions & 0 deletions label_studio/core/all_urls.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@
"name": "projects:api:project-detail",
"decorators": ""
},
{
"url": "/api/projects/counts/",
"module": "projects.api.ProjectCountsListAPI",
"name": "projects:api:project-counts-list",
"decorators": ""
},
{
"url": "/api/projects/<int:pk>/next/",
"module": "projects.api.ProjectNextTaskAPI",
Expand Down
18 changes: 12 additions & 6 deletions label_studio/core/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,14 @@
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
STATICFILES_STORAGE = 'core.storage.SkipMissedManifestStaticFilesStorage'
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
},
'staticfiles': {
'BACKEND': 'core.storage.SkipMissedManifestStaticFilesStorage',
},
}

# Sessions and CSRF
SESSION_COOKIE_SECURE = bool(int(get_env('SESSION_COOKIE_SECURE', False)))
Expand Down Expand Up @@ -648,7 +655,7 @@ def collect_versions_dummy(**kwargs):

if get_env('MINIO_STORAGE_ENDPOINT') and not get_bool_env('MINIO_SKIP', False):
CLOUD_FILE_STORAGE_ENABLED = True
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STORAGES['default']['BACKEND'] = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_STORAGE_BUCKET_NAME = get_env('MINIO_STORAGE_BUCKET_NAME')
AWS_ACCESS_KEY_ID = get_env('MINIO_STORAGE_ACCESS_KEY')
AWS_SECRET_ACCESS_KEY = get_env('MINIO_STORAGE_SECRET_KEY')
Expand All @@ -661,7 +668,7 @@ def collect_versions_dummy(**kwargs):

if get_env('STORAGE_TYPE') == 's3':
CLOUD_FILE_STORAGE_ENABLED = True
DEFAULT_FILE_STORAGE = 'core.storage.CustomS3Boto3Storage'
STORAGES['default']['BACKEND'] = 'core.storage.CustomS3Boto3Storage'
if get_env('STORAGE_AWS_ACCESS_KEY_ID'):
AWS_ACCESS_KEY_ID = get_env('STORAGE_AWS_ACCESS_KEY_ID')
if get_env('STORAGE_AWS_SECRET_ACCESS_KEY'):
Expand All @@ -681,7 +688,7 @@ def collect_versions_dummy(**kwargs):

if get_env('STORAGE_TYPE') == 'azure':
CLOUD_FILE_STORAGE_ENABLED = True
DEFAULT_FILE_STORAGE = 'core.storage.CustomAzureStorage'
STORAGES['default']['BACKEND'] = 'core.storage.CustomAzureStorage'
AZURE_ACCOUNT_NAME = get_env('STORAGE_AZURE_ACCOUNT_NAME')
AZURE_ACCOUNT_KEY = get_env('STORAGE_AZURE_ACCOUNT_KEY')
AZURE_CONTAINER = get_env('STORAGE_AZURE_CONTAINER_NAME')
Expand All @@ -690,8 +697,7 @@ def collect_versions_dummy(**kwargs):

if get_env('STORAGE_TYPE') == 'gcs':
CLOUD_FILE_STORAGE_ENABLED = True
# DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
DEFAULT_FILE_STORAGE = 'core.storage.AlternativeGoogleCloudStorage'
STORAGES['default']['BACKEND'] = 'core.storage.AlternativeGoogleCloudStorage'
GS_PROJECT_ID = get_env('STORAGE_GCS_PROJECT_ID')
GS_BUCKET_NAME = get_env('STORAGE_GCS_BUCKET_NAME')
GS_EXPIRATION = timedelta(seconds=int(get_env('STORAGE_GCS_EXPIRATION_SECS', '86400')))
Expand Down
2 changes: 1 addition & 1 deletion label_studio/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def version_page(request):
# html / json response
if request.path == '/version/':
# other settings from backend
if request.user.is_superuser:
if not getattr(settings, 'CLOUD_INSTANCE', False) and request.user.is_superuser:
result['settings'] = {
key: str(getattr(settings, key))
for key in dir(settings)
Expand Down
10 changes: 5 additions & 5 deletions label_studio/data_manager/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ def annotate_annotations_results(queryset):
)
)
else:
return queryset.annotate(annotations_results=ArrayAgg('annotations__result', distinct=True))
return queryset.annotate(annotations_results=ArrayAgg('annotations__result', distinct=True, default=Value([])))


def annotate_predictions_results(queryset):
Expand All @@ -610,7 +610,7 @@ def annotate_predictions_results(queryset):
)
)
else:
return queryset.annotate(predictions_results=ArrayAgg('predictions__result', distinct=True))
return queryset.annotate(predictions_results=ArrayAgg('predictions__result', distinct=True, default=Value([])))


def annotate_annotators(queryset):
Expand All @@ -619,7 +619,7 @@ def annotate_annotators(queryset):
annotators=Coalesce(GroupConcat('annotations__completed_by'), Value(''), output_field=models.CharField())
)
else:
return queryset.annotate(annotators=ArrayAgg('annotations__completed_by', distinct=True))
return queryset.annotate(annotators=ArrayAgg('annotations__completed_by', distinct=True, default=Value([])))


def annotate_predictions_score(queryset):
Expand Down Expand Up @@ -653,7 +653,7 @@ def annotate_annotations_ids(queryset):
if settings.DJANGO_DB == settings.DJANGO_DB_SQLITE:
return queryset.annotate(annotations_ids=GroupConcat('annotations__id', output_field=models.CharField()))
else:
return queryset.annotate(annotations_ids=ArrayAgg('annotations__id'))
return queryset.annotate(annotations_ids=ArrayAgg('annotations__id', default=Value([])))


def annotate_predictions_model_versions(queryset):
Expand All @@ -662,7 +662,7 @@ def annotate_predictions_model_versions(queryset):
predictions_model_versions=GroupConcat('predictions__model_version', output_field=models.CharField())
)
else:
return queryset.annotate(predictions_model_versions=ArrayAgg('predictions__model_version'))
return queryset.annotate(predictions_model_versions=ArrayAgg('predictions__model_version', default=Value([])))


def annotate_avg_lead_time(queryset):
Expand Down
31 changes: 29 additions & 2 deletions label_studio/feature_flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -2418,6 +2418,33 @@
"version": 4,
"deleted": false
},
"fflag_feat_front_dia_1747_projects_list_banner": {
"key": "fflag_feat_front_dia_1747_projects_list_banner",
"on": false,
"prerequisites": [],
"targets": [],
"contextTargets": [],
"rules": [],
"fallthrough": {
"variation": 0
},
"offVariation": 1,
"variations": [
true,
false
],
"clientSideAvailability": {
"usingMobileKey": false,
"usingEnvironmentId": false
},
"clientSide": false,
"salt": "21bfa474c82b4ab0b89314fbecd164cc",
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 3,
"deleted": false
},
"fflag_feat_front_dia_916_model_creation_and_versioning_short": {
"key": "fflag_feat_front_dia_916_model_creation_and_versioning_short",
"on": true,
Expand Down Expand Up @@ -3036,7 +3063,7 @@
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 2,
"version": 3,
"deleted": false
},
"fflag_feat_front_optic_1419_backend_csv_comments_export_short": {
Expand Down Expand Up @@ -4778,7 +4805,7 @@
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 2,
"version": 3,
"deleted": false
},
"fflag_fix_leap_246_multi_object_hotkeys_160124_short": {
Expand Down
31 changes: 31 additions & 0 deletions label_studio/projects/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from projects.models import Project, ProjectImport, ProjectManager, ProjectReimport, ProjectSummary
from projects.serializers import (
GetFieldsSerializer,
ProjectCountsSerializer,
ProjectImportSerializer,
ProjectLabelConfigSerializer,
ProjectModelVersionExtendedSerializer,
Expand Down Expand Up @@ -281,6 +282,36 @@ def post(self, request, *args, **kwargs):
return super(ProjectListAPI, self).post(request, *args, **kwargs)


@method_decorator(
name='get',
decorator=swagger_auto_schema(
tags=['Projects'],
x_fern_sdk_group_name='projects',
x_fern_sdk_method_name='counts',
x_fern_audiences=['public'],
x_fern_pagination={
'offset': '$request.page',
'results': '$response.results',
},
operation_summary="List project's counts",
operation_description='Returns a list of projects with their counts. For example, task_number which is the total task number in project',
),
)
class ProjectCountsListAPI(generics.ListAPIView):
serializer_class = ProjectCountsSerializer
filterset_class = ProjectFilterSet
permission_required = ViewClassPermission(
GET=all_permissions.projects_view,
)
pagination_class = ProjectListPagination

def get_queryset(self):
serializer = GetFieldsSerializer(data=self.request.query_params)
serializer.is_valid(raise_exception=True)
fields = serializer.validated_data.get('include')
return Project.objects.with_counts(fields=fields).filter(organization=self.request.user.active_organization)


@method_decorator(
name='get',
decorator=swagger_auto_schema(
Expand Down
99 changes: 99 additions & 0 deletions label_studio/projects/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
import bleach
from constants import SAFE_HTML_ATTRIBUTES, SAFE_HTML_TAGS
from django.db.models import Q
from label_studio_sdk.label_interface import LabelInterface
from label_studio_sdk.label_interface.control_tags import (
BrushLabelsTag,
BrushTag,
ChoicesTag,
DateTimeTag,
EllipseLabelsTag,
EllipseTag,
HyperTextLabelsTag,
KeyPointLabelsTag,
KeyPointTag,
LabelsTag,
NumberTag,
ParagraphLabelsTag,
PolygonLabelsTag,
PolygonTag,
RatingTag,
RectangleLabelsTag,
RectangleTag,
TaxonomyTag,
TextAreaTag,
TimeSeriesLabelsTag,
VideoRectangleTag,
)
from projects.models import Project, ProjectImport, ProjectOnboarding, ProjectReimport, ProjectSummary
from rest_flex_fields import FlexFieldsModelSerializer
from rest_framework import serializers
Expand Down Expand Up @@ -66,6 +90,9 @@ class ProjectSerializer(FlexFieldsModelSerializer):
config_has_control_tags = SerializerMethodField(
default=None, read_only=True, help_text='Flag to detect is project ready for labeling'
)
config_suitable_for_bulk_annotation = serializers.SerializerMethodField(
default=None, read_only=True, help_text='Flag to detect is project ready for bulk annotation'
)
finished_task_number = serializers.IntegerField(default=None, read_only=True, help_text='Finished tasks')

queue_total = serializers.SerializerMethodField()
Expand All @@ -82,6 +109,61 @@ def user_id(self):
def get_config_has_control_tags(project):
return len(project.get_parsed_config()) > 0

@staticmethod
def get_config_suitable_for_bulk_annotation(project):
li = LabelInterface(project.label_config)

# List of tags that should not be present
disallowed_tags = [
LabelsTag,
BrushTag,
BrushLabelsTag,
EllipseTag,
EllipseLabelsTag,
KeyPointTag,
KeyPointLabelsTag,
PolygonTag,
PolygonLabelsTag,
RectangleTag,
RectangleLabelsTag,
HyperTextLabelsTag,
ParagraphLabelsTag,
TimeSeriesLabelsTag,
VideoRectangleTag,
]

# Return False if any disallowed tag is present
for tag_class in disallowed_tags:
if li.find_tags_by_class(tag_class):
return False

# Check perRegion/perItem for expanded list of tags, plus value="no" for Choices/Taxonomy
allowed_tags_for_checks = [ChoicesTag, TaxonomyTag, DateTimeTag, NumberTag, RatingTag, TextAreaTag]
for tag_class in allowed_tags_for_checks:
tags = li.find_tags_by_class(tag_class)
for tag in tags:
per_region = tag.attr.get('perRegion', 'false').lower() == 'true'
per_item = tag.attr.get('perItem', 'false').lower() == 'true'
if per_region or per_item:
return False
# For ChoicesTag and TaxonomyTag, the value attribute must not be set at all
if tag_class in [ChoicesTag, TaxonomyTag]:
if 'value' in tag.attr:
return False

# For TaxonomyTag, check labeling and apiUrl
taxonomy_tags = li.find_tags_by_class(TaxonomyTag)
for tag in taxonomy_tags:
labeling = tag.attr.get('labeling', 'false').lower() == 'true'
if labeling:
return False
api_url = tag.attr.get('apiUrl', None)
if api_url is not None:
return False

# If all checks pass, return True
return True

@staticmethod
def get_parsed_label_config(project):
return project.get_parsed_config()
Expand Down Expand Up @@ -156,6 +238,7 @@ class Meta:
'finished_task_number',
'queue_total',
'queue_done',
'config_suitable_for_bulk_annotation',
]

def validate_label_config(self, value):
Expand Down Expand Up @@ -214,6 +297,22 @@ def get_queue_done(self, project):
return result


class ProjectCountsSerializer(ProjectSerializer):
class Meta:
model = Project
fields = [
'id',
'task_number',
'finished_task_number',
'total_predictions_number',
'total_annotations_number',
'num_tasks_with_annotations',
'useful_annotation_number',
'ground_truth_number',
'skipped_annotations_number',
]


class ProjectOnboardingSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectOnboarding
Expand Down
Loading

0 comments on commit dfabd62

Please sign in to comment.