22Content Tagging APIs
33"""
44from __future__ import annotations
5+ import io
56
67from itertools import groupby
8+ import csv
9+ from typing import Iterator
10+ from opaque_keys .edx .keys import UsageKey
711
812import openedx_tagging .core .tagging .api as oel_tagging
913from django .db .models import Exists , OuterRef , Q , QuerySet
1014from opaque_keys .edx .keys import CourseKey
1115from opaque_keys .edx .locator import LibraryLocatorV2
1216from openedx_tagging .core .tagging .models import ObjectTag , Taxonomy
17+ from openedx_tagging .core .tagging .models .utils import TAGS_CSV_SEPARATOR
1318from organizations .models import Organization
19+ from .helpers .objecttag_export_helpers import build_object_tree_with_objecttags , iterate_with_level
1420
1521from .models import TaxonomyOrg
1622from .types import ContentKey , TagValuesByObjectIdDict , TagValuesByTaxonomyIdDict , TaxonomyDict
@@ -164,7 +170,7 @@ def get_all_object_tags(
164170 all_object_tags = ObjectTag .objects .filter (
165171 Q (tag__isnull = False , tag__taxonomy__isnull = False ),
166172 object_id_clause ,
167- ).select_related ("tag__taxonomy" )
173+ ).select_related ("tag__taxonomy" ). order_by ( "object_id" )
168174
169175 if prefetch_orgs :
170176 all_object_tags = all_object_tags .prefetch_related ("tag__taxonomy__taxonomyorg_set" )
@@ -174,7 +180,8 @@ def get_all_object_tags(
174180
175181 for object_id , block_tags in groupby (all_object_tags , lambda x : x .object_id ):
176182 grouped_object_tags [object_id ] = {}
177- for taxonomy_id , taxonomy_tags in groupby (block_tags , lambda x : x .tag .taxonomy_id if x .tag else 0 ):
183+ block_tags_sorted = sorted (block_tags , key = lambda x : x .tag .taxonomy_id if x .tag else 0 ) # type: ignore
184+ for taxonomy_id , taxonomy_tags in groupby (block_tags_sorted , lambda x : x .tag .taxonomy_id if x .tag else 0 ):
178185 object_tags_list = list (taxonomy_tags )
179186 grouped_object_tags [object_id ][taxonomy_id ] = [
180187 tag .value for tag in object_tags_list
@@ -185,7 +192,7 @@ def get_all_object_tags(
185192 assert object_tags_list [0 ].tag .taxonomy
186193 taxonomies [taxonomy_id ] = object_tags_list [0 ].tag .taxonomy
187194
188- return grouped_object_tags , taxonomies
195+ return grouped_object_tags , dict ( sorted ( taxonomies . items ()))
189196
190197
191198def set_all_object_tags (
@@ -211,6 +218,125 @@ def set_all_object_tags(
211218 )
212219
213220
221+ def generate_csv_rows (object_id , buffer ) -> Iterator [str ]:
222+ """
223+ Returns a CSV string with tags and taxonomies of all blocks of `object_id`
224+ """
225+ content_key = get_content_key_from_string (object_id )
226+
227+ if isinstance (content_key , UsageKey ):
228+ raise ValueError ("The object_id must be a CourseKey or a LibraryLocatorV2." )
229+
230+ all_object_tags , taxonomies = get_all_object_tags (content_key )
231+ tagged_content = build_object_tree_with_objecttags (content_key , all_object_tags )
232+
233+ header = {"name" : "Name" , "type" : "Type" , "id" : "ID" }
234+
235+ # Prepare the header for the taxonomies
236+ for taxonomy_id , taxonomy in taxonomies .items ():
237+ header [f"taxonomy_{ taxonomy_id } " ] = taxonomy .export_id
238+
239+ csv_writer = csv .DictWriter (buffer , fieldnames = header .keys (), quoting = csv .QUOTE_NONNUMERIC )
240+ yield csv_writer .writerow (header )
241+
242+ # Iterate over the blocks and yield the rows
243+ for item , level in iterate_with_level (tagged_content ):
244+ block_key = get_content_key_from_string (item .block_id )
245+
246+ block_data = {
247+ "name" : level * " " + item .display_name ,
248+ "type" : item .category ,
249+ "id" : getattr (block_key , 'block_id' , item .block_id ),
250+ }
251+
252+ # Add the tags for each taxonomy
253+ for taxonomy_id in taxonomies :
254+ if taxonomy_id in item .object_tags :
255+ block_data [f"taxonomy_{ taxonomy_id } " ] = f"{ TAGS_CSV_SEPARATOR } " .join (
256+ list (item .object_tags [taxonomy_id ])
257+ )
258+
259+ yield csv_writer .writerow (block_data )
260+
261+
262+ def export_tags_in_csv_file (object_id , file_dir , file_name ) -> None :
263+ """
264+ Writes a CSV file with tags and taxonomies of all blocks of `object_id`
265+ """
266+ buffer = io .StringIO ()
267+ for _ in generate_csv_rows (object_id , buffer ):
268+ # The generate_csv_rows function is a generator,
269+ # we don't need to do anything with the result here
270+ pass
271+
272+ with file_dir .open (file_name , 'w' ) as csv_file :
273+ buffer .seek (0 )
274+ csv_file .write (buffer .read ())
275+
276+
277+ def set_exported_object_tags (
278+ content_key : ContentKey ,
279+ exported_tags : TagValuesByTaxonomyIdDict ,
280+ ) -> None :
281+ """
282+ Sets the tags for the given exported content object.
283+ """
284+ content_key_str = str (content_key )
285+
286+ # Clear all tags related with the content.
287+ oel_tagging .delete_object_tags (content_key_str )
288+
289+ for taxonomy_export_id , tags_values in exported_tags .items ():
290+ if not tags_values :
291+ continue
292+
293+ taxonomy = oel_tagging .get_taxonomy_by_export_id (str (taxonomy_export_id ))
294+ oel_tagging .tag_object (
295+ object_id = content_key_str ,
296+ taxonomy = taxonomy ,
297+ tags = tags_values ,
298+ create_invalid = True ,
299+ taxonomy_export_id = str (taxonomy_export_id ),
300+ )
301+
302+
303+ def import_course_tags_from_csv (csv_path , course_id ) -> None :
304+ """
305+ Import tags from a csv file generated on export.
306+ """
307+ # Open csv file and extract the tags
308+ with open (csv_path , 'r' ) as csv_file :
309+ csv_reader = csv .DictReader (csv_file )
310+ tags_in_blocks = list (csv_reader )
311+
312+ def get_exported_tags (block ) -> TagValuesByTaxonomyIdDict :
313+ """
314+ Returns a map with taxonomy export_id and tags for this block.
315+ """
316+ result = {}
317+ for key , value in block .items ():
318+ if key in ['Type' , 'Name' , 'ID' ] or not value :
319+ continue
320+ result [key ] = value .split (TAGS_CSV_SEPARATOR )
321+ return result
322+
323+ course_key = CourseKey .from_string (str (course_id ))
324+
325+ for block in tags_in_blocks :
326+ exported_tags = get_exported_tags (block )
327+ block_type = block .get ('Type' , '' )
328+ block_id = block .get ('ID' , '' )
329+
330+ if not block_type or not block_id :
331+ raise ValueError (f"Invalid format of csv in: '{ block } '." )
332+
333+ if block_type == 'course' :
334+ set_exported_object_tags (course_key , exported_tags )
335+ else :
336+ block_key = course_key .make_usage_key (block_type , block_id )
337+ set_exported_object_tags (block_key , exported_tags )
338+
339+
214340def copy_object_tags (
215341 source_content_key : ContentKey ,
216342 dest_content_key : ContentKey ,
0 commit comments