From 1f3f0b6ca98f4cfe44e779cf565a46ef711cb046 Mon Sep 17 00:00:00 2001 From: nik Date: Thu, 16 Jan 2025 09:22:07 -0800 Subject: [PATCH 1/3] feat: DIA-1815: Add conditional dependencies in product tour --- label_studio/users/product_tours/serializers.py | 15 +++++++++++++-- web/libs/core/src/lib/Tour/TourProvider.tsx | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/label_studio/users/product_tours/serializers.py b/label_studio/users/product_tours/serializers.py index 0681c49fa43e..f512c12812bc 100644 --- a/label_studio/users/product_tours/serializers.py +++ b/label_studio/users/product_tours/serializers.py @@ -1,7 +1,7 @@ import pathlib -from functools import cached_property - import yaml +from functools import cached_property +from core.utils.db import fast_first from rest_framework import serializers from .models import ProductTourInteractionData, UserProductTour @@ -11,6 +11,7 @@ class UserProductTourSerializer(serializers.ModelSerializer): steps = serializers.SerializerMethodField(read_only=True) + awaiting = serializers.SerializerMethodField(read_only=True) class Meta: model = UserProductTour @@ -29,11 +30,21 @@ def validate_name(self, value): return value + @cached_property def load_tour_config(self): # TODO: get product tour from yaml file. Later we move it to remote storage, e.g. S3 filepath = PRODUCT_TOURS_CONFIGS_DIR / f'{self.context["name"]}.yml' with open(filepath, 'r') as f: return yaml.safe_load(f) + + def get_awaiting(self, obj): + config = self.load_tour_config() + dependencies = config.get('dependencies', []) + for dependency in dependencies: + tour = fast_first(UserProductTour.objects.filter(user=self.context['request'].user, name=dependency)) + if not tour or tour.status != UserProductTour.Status.COMPLETED: + return True + return False def get_steps(self, obj): config = self.load_tour_config() diff --git a/web/libs/core/src/lib/Tour/TourProvider.tsx b/web/libs/core/src/lib/Tour/TourProvider.tsx index ae3167c97d4d..bfbb96a34d38 100644 --- a/web/libs/core/src/lib/Tour/TourProvider.tsx +++ b/web/libs/core/src/lib/Tour/TourProvider.tsx @@ -131,6 +131,11 @@ export const TourProvider: React.FC<{ return; } + if (response.awaiting) { + console.info(`Tour "${name}" is awaiting other tours`); + return; + } + if (!response.steps?.length) { console.info(`No steps found for tour "${name}"`); return; From ad420799a33f8b0ce5bdc4c98badb003ba865747 Mon Sep 17 00:00:00 2001 From: nik Date: Thu, 16 Jan 2025 12:27:52 -0800 Subject: [PATCH 2/3] fix state check --- label_studio/users/product_tours/serializers.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/label_studio/users/product_tours/serializers.py b/label_studio/users/product_tours/serializers.py index f512c12812bc..18e3bb02959a 100644 --- a/label_studio/users/product_tours/serializers.py +++ b/label_studio/users/product_tours/serializers.py @@ -1,10 +1,14 @@ +import logging import pathlib -import yaml from functools import cached_property + +import yaml from core.utils.db import fast_first from rest_framework import serializers -from .models import ProductTourInteractionData, UserProductTour +from .models import ProductTourInteractionData, ProductTourState, UserProductTour + +logger = logging.getLogger(__name__) PRODUCT_TOURS_CONFIGS_DIR = pathlib.Path(__file__).parent / 'configs' @@ -36,18 +40,19 @@ def load_tour_config(self): filepath = PRODUCT_TOURS_CONFIGS_DIR / f'{self.context["name"]}.yml' with open(filepath, 'r') as f: return yaml.safe_load(f) - + def get_awaiting(self, obj): - config = self.load_tour_config() + config = self.load_tour_config dependencies = config.get('dependencies', []) for dependency in dependencies: tour = fast_first(UserProductTour.objects.filter(user=self.context['request'].user, name=dependency)) - if not tour or tour.status != UserProductTour.Status.COMPLETED: + if not tour or tour.state != ProductTourState.COMPLETED: + logger.info(f'Tour {dependency} is not completed: skipping tour {self.context["name"]}') return True return False def get_steps(self, obj): - config = self.load_tour_config() + config = self.load_tour_config return config.get('steps', []) def validate_interaction_data(self, value): From 6221142fef9854cfe602e9e71ff26ca0dba99d41 Mon Sep 17 00:00:00 2001 From: nik Date: Thu, 16 Jan 2025 12:31:22 -0800 Subject: [PATCH 3/3] Add comments --- label_studio/users/product_tours/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/label_studio/users/product_tours/serializers.py b/label_studio/users/product_tours/serializers.py index 18e3bb02959a..d600d5212d19 100644 --- a/label_studio/users/product_tours/serializers.py +++ b/label_studio/users/product_tours/serializers.py @@ -14,7 +14,9 @@ class UserProductTourSerializer(serializers.ModelSerializer): + # steps is a list of steps in the tour loaded from the yaml file steps = serializers.SerializerMethodField(read_only=True) + # awaiting is a boolean that indicates if the tour is awaiting other tours in the list of "dependencies" awaiting = serializers.SerializerMethodField(read_only=True) class Meta: