source: products/qPloneSkinDump/trunk/skin_template/utils.py @ 2111

Last change on this file since 2111 was 74, checked in by chervol, 15 years ago

initial plone2.0.5 branch import

  • Property svn:eol-style set to native
File size: 23.5 KB
Line 
1import transaction
2import os, sys, re, string
3from sets import Set
4from StringIO import StringIO
5from time import gmtime, strftime
6from zLOG import LOG, INFO
7from zExceptions import BadRequest
8from App.config import getConfiguration
9from Products.CMFCore.utils import getToolByName
10from Products.CMFCore.DirectoryView import addDirectoryViews
11from Products.%(SKIN_PRODUCT_NAME)s.config import *
12from fixes import fix
13
14######################################################################
15##                      IMPORTING UTILS                             ##
16######################################################################
17osp = os.path
18ALLOWED_IMPORT_POLICY = ["only_new", "backup", "overwrite"]
19INTRO_TO_INSTANCE = "< Started copying object files from Product import directory to Instance one."
20SUMMARY_TO_INSTANCE = "> Finished copying."
21INTRO_TO_ROOT = "< Started import %%s file[s] with '%%s' policy."
22SUMMARY_TO_ROOT = "> Finished importing."
23INTRO_CLEAN = "< Started cleaning Instance import directory."
24SUMMARY_CLEAN = "> Finished cleaning."
25CREXP_INVALID_ID = re.compile('^The id \"(.*?)\" is invalid - it is already in use.$', re.DOTALL|re.IGNORECASE|re.MULTILINE)
26CSS_BASE_IDS_QPSD053 = ['id','expression','enabled','cookable','media','rel','title','rendering']   # supporting qPSD-0.5.3 version
27################    CHECK IMPORTING    ################
28def checkIfImport():
29    """ Return if perform importing, based on checking
30        *zexp files in <SkinProduct>/import directory.
31    """
32    instance_ipath, product_ipath = getImportedPathes()
33    product_ilist = [i for i in os.listdir(product_ipath) \
34                     if osp.isfile(osp.join(product_ipath,i)) and i.endswith('.zexp')]
35    if product_ilist:
36        return 1
37    return 0
38
39################    IMPORTING TO PLONE'S IMPORT DIR   ################
40def getImportedPathes():
41    """ Return Plone instance and Skin product import pathes."""
42    # Based on instance path, construct import pathes
43    cfg = getConfiguration()
44    instance_ipath = osp.join(cfg.instancehome, "import")
45    product_ipath = osp.join(PRODUCTS_PATH, PRODUCT_NAME, "import")
46    # Check presence of Product import directory
47    if not osp.isdir(product_ipath):       
48        raise BadRequest, "Skin Product's import directory '%%s' - does not exist or is'nt direcory" %% product_ipath
49    # Check presence of Instance import directory
50    if not osp.isdir(instance_ipath):
51        raise BadRequest, "Instance import directory '%%s' - does not exist or isn't direcory" %% instance_ipath
52    return [instance_ipath, product_ipath]
53
54def copyFile(src_dir, dst_dir, f_name):
55    """ Copy file from src_dir to dst_dir under original name."""
56    try:
57        src_file = open(osp.join(src_dir, f_name),"rb")
58        dst_file = open(osp.join(dst_dir, f_name),"wb")
59        dst_file.write(src_file.read())
60        dst_file.close()
61        src_file.close()
62    except Exception, e:
63        msg = "!!! In copying files from <%%s> dir to <%%s> dir exception occur. Details: %%s." %% (src_dir,dst_dir, str(e))
64        print >> import_out, msg
65        LOG('performImportToPortal',INFO,'copyFile', msg)
66
67def moveToTemp(same_instance_files, instance_ipath, temp_dir_path):
68    """ Move samenamed files from Instanse's dir to temp dir."""
69    os.mkdir(temp_dir_path) # Create temp back_[date] dir
70    try:
71        [copyFile(instance_ipath, temp_dir_path, f_name) for f_name in same_instance_files]
72        [os.remove(osp.join(instance_ipath, f_name)) for f_name in same_instance_files]
73    except Exception, e:
74        msg = "!!! Exception occur during moving files from Instance's dir to temp dir. Detaile:%%s." %% str(e)
75        print >> import_out, msg
76        LOG('performImportToPortal',INFO,'moveToTemp', msg)
77   
78def copyToInstanceImport():
79    """ Perform copying imported files from <SkinProduct>/import dir
80        to Plone's instance import dir.
81    """
82    print >> import_out, INTRO_TO_INSTANCE
83    instance_ipath, product_ipath = getImportedPathes()
84    # Compose temp dir back_[date] dir path in Instance import directory
85    temp_dir_id = "back_%%s" %% strftime("%%Y%%m%%d%%H%%M%%S", gmtime())
86    temp_dir_path = osp.join(instance_ipath, temp_dir_id)
87    # Get *.zexp files from Skin Product's import dir and Plone's instance import dir files
88    product_ilist = [i for i in os.listdir(product_ipath) \
89                     if osp.isfile(osp.join(product_ipath,i)) and i.endswith('.zexp')]
90    instance_ilist = [i for i in os.listdir(instance_ipath) \
91                      if osp.isfile(osp.join(instance_ipath,i)) and i.endswith('.zexp')]
92    # Check for presence samenamed files in Instance and Product import directories.
93    same_instance_files = [f_name for f_name in instance_ilist if f_name in product_ilist]
94    if same_instance_files:
95        moveToTemp(same_instance_files, instance_ipath, temp_dir_path)
96    # Copy all *zexp files from Product's import dir to Instance's import dir
97    [copyFile(product_ipath, instance_ipath, f_name) for f_name in product_ilist]
98    print >> import_out, SUMMARY_TO_INSTANCE
99    return [instance_ipath, product_ipath, temp_dir_path, product_ilist]
100
101################    IMPORTING TO PORTAL   ################
102def importObject(portal, file_name):
103    """ Work around old Zope bug in importing."""
104    try:
105        portal.manage_importObject(file_name)
106    except:
107        portal._p_jar = portal.Destination()._p_jar
108        portal.manage_importObject(file_name)
109
110def makeBackUp(portal, portal_objects, temp_dir_path, obj_id):
111    """ Perfom backup same named portal objects in temp folder."""
112    # Get id of temp folder-object
113    durty_path,temp_id = osp.split(temp_dir_path)
114    if not temp_id:
115        durty_path,temp_id = osp.split(durty_path)
116    # Get temp folder-object
117    if temp_id not in portal_objects:
118        portal.invokeFactory('Large Plone Folder', id=temp_id)
119        print >> import_out, "! Created '%%s' backup directory with same-ids " \
120                             "objects from portal root." %% temp_id
121    temp_dir = getattr(portal, temp_id)
122    # Move object with same id to temp folder-object
123    #get_transaction().commit(1)
124    transaction.savepoint()
125    obj = portal.manage_cutObjects(ids=[obj_id])
126    temp_dir.manage_pasteObjects(obj)
127    print >> import_out, "! '%%s' Object moved from portal root to '%%s' backup directory." %% (obj_id, temp_id)
128
129def performImport(portal, temp_dir_path, file_name):
130    """ Importing an object to portal."""
131    portal_objects = portal.objectIds()
132    try:
133        portal.manage_importObject(file_name)
134    except Exception, e:
135        msg = str(e)
136        is_invalid_id = CREXP_INVALID_ID.match(msg)
137        if is_invalid_id:
138            obj_id = is_invalid_id.group(1)
139            if IMPORT_POLICY == "only_new":
140                msg = "! Object with '%%s' id was not importing because it's already exist " \
141                      "in portal root." %% obj_id
142                print >> import_out, msg
143            elif IMPORT_POLICY == "backup":
144                makeBackUp(portal, portal_objects, temp_dir_path, obj_id)
145                importObject(portal, file_name)
146            elif IMPORT_POLICY == "overwrite":
147                portal.manage_delObjects(ids=[obj_id])
148                importObject(portal, file_name)
149        else:
150            # work around old Zope bug in importing
151            portal._p_jar = portal.Destination()._p_jar
152            portal.manage_importObject(file_name)
153
154def importToPortalRoot(portal, product_file_names, temp_dir_path):
155    """ Import all objects from *zexp files to portal root (based on IMPORT_POLICY)."""
156    if not IMPORT_POLICY in ALLOWED_IMPORT_POLICY:
157        raise Exception("%%s - wrong import policy in '%%s/config.py' file. Must be one of the %%s" \
158                        %% (IMPORT_POLICY, PRODUCT_NAME, ALLOWED_IMPORT_POLICY) )
159    print >> import_out, INTRO_TO_ROOT %% (product_file_names, IMPORT_POLICY)
160    for file_name in product_file_names:
161        try:
162            performImport(portal, temp_dir_path, file_name)
163        except Exception, error:
164            msg = '!!! Under "%%s" policy importing exception occur: %%s.' %% (IMPORT_POLICY, str(error))
165            print >> import_out, msg
166            LOG('performImportToPortal',INFO,'importToPortalRoot', msg)
167    print >> import_out, SUMMARY_TO_ROOT
168
169################    CLEANING PLONE'S IMPORT DIR   ################
170def cleanInstanceImport(instance_ipath, product_file_names, temp_dir_path):
171    """ Cleaning Plone's import dir."""
172    print >> import_out, INTRO_CLEAN
173    # Erase all copied *zexp files from Instance's import dir
174    for f_name in product_file_names:
175        f_path = osp.join(instance_ipath, f_name)
176        if osp.exists(f_path) and osp.isfile(f_path):
177            os.remove(f_path)
178        else:
179            msg = '! "%%s" file was not deleted from "%%s" import directory.' %%\
180                   (f_name, osp.join(instance_ipath))
181            print >> import_out, msg
182            LOG('performImportToPortal',INFO,'cleanInstanceImport', msg)
183    # Move all files from temp back_[date] dir to Instance's import dir
184    if osp.exists(temp_dir_path) and osp.isdir(temp_dir_path):
185        f_names = os.listdir(temp_dir_path)
186        try:
187            [copyFile(temp_dir_path, instance_ipath, f_name) for f_name in f_names]
188            [os.remove(osp.join(temp_dir_path, f_name)) for f_name in f_names]
189            # Erase temp back_[date] dir
190            os.rmdir(temp_dir_path)
191        except Exception, e:
192            msg = "!!! In moving files from temp dir to Instance's import dir exception occur."
193            print >> import_out, msg
194            LOG('performImportToPortal',INFO,'moveFromTempToImport', msg)
195    print >> import_out, SUMMARY_CLEAN
196
197def fixImportingIssues(portal, beforeimporting_objects):
198    ''' Fix defects of importing process: reindexing, other'''
199    afterimporting_objects = portal.objectItems()
200    diff_objects = list(Set(afterimporting_objects)-Set(beforeimporting_objects))
201    for id, ob in diff_objects:
202        if id.startswith('back_'):
203            continue
204        fix(ob)
205
206################    MAIN    ################
207def performImportToPortal(portal):
208    """ Import objects from Skin Product to Portal root."""
209    globals()['import_out'] = StringIO()
210    instance_ipath, product_ipath, temp_dir_path, product_file_names = copyToInstanceImport()
211    if product_file_names:
212        beforeimporting_objects = portal.objectItems()
213        importToPortalRoot(portal, product_file_names, temp_dir_path)
214        fixImportingIssues(portal, beforeimporting_objects)
215        cleanInstanceImport(instance_ipath, product_file_names, temp_dir_path)
216    else:
217        print >> import_out, "!!! Failure importing: there is no file for importing to be found."
218    result = import_out
219    del globals()['import_out']
220    return result.getvalue()
221
222######################################################################
223##              INSTALLATION/UNINSTALLATION UTILS                   ##
224######################################################################
225CSS_REG_PROPS = ['id', 'expression', 'enabled', 'cookable', 'cacheable' \
226                ,'media', 'rel', 'title', 'rendering', 'compression']
227JS_REG_PROPS = ['id', 'expression', 'enabled', 'cookable', 'cacheable' \
228               ,'inline', 'compression']
229
230def installSkin(portal, pp_up, out):
231    # Checking for presense SKIN_NAME in portal_skins directory view or among Skin Names
232    skinsTool = getToolByName(portal, 'portal_skins')
233    # Get unique product_skin_name and remember it in case of differ from SKIN_NAME.
234    product_skin_name = SKIN_NAME
235    skin_names = skinsTool.getSkinSelections()
236    if product_skin_name in skin_names:
237        idx = 0
238        while product_skin_name in skin_names:
239            product_skin_name = SKIN_NAME + str(idx)
240            idx += 1
241        addProperty(pp_up, 'q_actual_skin_name', product_skin_name, 'string', out)
242    # Add directory views
243    layer_skin_name = string.lower(SKIN_NAME)
244    addDirectoryViews(skinsTool, 'skins', GLOBALS)
245    print >> out,  "- added '%%s' directory views to portal_skins." %% layer_skin_name
246    # Get Default skin and remember it for backup on uninstallig
247    default_skin = skinsTool.getDefaultSkin()
248    addProperty(pp_up, 'q_default_skin', default_skin, 'string', out)
249    # Building list of layers for NEW SKIN
250    base_path = skinsTool.getSkinPath(BASE_SKIN_NAME)
251    new_path = map( string.strip, string.split(base_path,',') )
252    if layer_skin_name in new_path :
253        print >> out, "- %%s layer already present in '%%s' skin." %% (layer_skin_name, BASE_SKIN_NAME)
254        # Remove layer_skin_name from current position.
255        del new_path[new_path.index(layer_skin_name)]
256    # Add layer_skin_name just after 'custom' position
257    try: 
258        new_path.insert(new_path.index('custom')+1, layer_skin_name)
259    except ValueError:
260        new_path.append(layer_skin_name)
261    new_path = string.join(new_path, ', ')
262    # Add NEW Skin and set it as dafault
263    skinsTool.addSkinSelection(product_skin_name, new_path, make_default=1)
264    print >> out, "Added %%s skin, bassed on %%s and set as default." %% (product_skin_name, BASE_SKIN_NAME)
265
266def uninstallSkin(skinsTool, actual_skin_name, initial_skin):
267    # Get 'portal_skins' object and list available skin names
268    # And remove SKIN_NAME from available skins, if it present
269    skin_names = skinsTool.getSkinSelections()
270    if actual_skin_name in skin_names :
271        skinsTool.manage_skinLayers(chosen=(actual_skin_name,), del_skin=1, REQUEST=None)
272        skin_names.remove(actual_skin_name)
273    # Remove product skin directory from skins tool
274    # AND Remove skin-product layer from available skins
275    skin_layer = SKIN_NAME.lower()
276    if skin_layer in skinsTool.objectIds():
277        skinsTool.manage_delObjects(skin_layer)
278    for skin_name in skin_names:
279        path = skinsTool.getSkinPath(skin_name)
280        path = [i.strip() for i in  path.split(',')]
281        if skin_layer in path:
282            path.remove(skin_layer)
283            path = ','.join(path)
284            skinsTool.addSkinSelection(skin_name, path)
285    # If current default skin == actual_skin_name
286    # Set default skin in initial one (if initial skin still exist)
287    # or in 1st from available skin names list.
288    current_default_skin = skinsTool.getDefaultSkin()
289    if current_default_skin == actual_skin_name:
290        if initial_skin in skin_names :
291            skinsTool.manage_properties(default_skin=initial_skin, REQUEST=None)
292        elif len(skin_names)>0 :
293            skinsTool.manage_properties(default_skin=skin_names[0], REQUEST=None)
294
295def addProperty(p_sheet, p_id, p_value, p_type, out):
296    if p_sheet.hasProperty(p_id):
297        p_sheet._delProperty(p_id)
298    p_sheet._setProperty(p_id, p_value, p_type)
299    print >> out, "... added %%s PropertySheet to %%s." %% (p_id, p_sheet.getId())
300
301def getResourceProperties(obj, prop_list, dflt=''):
302    """ Return list of 2 items list-[property name, property value]."""
303    properties=[]
304    for prop in prop_list:
305        accessor = getattr(obj, 'get%%s' %% prop.capitalize(), None)
306        if accessor:
307            properties.append([prop, accessor() or dflt])
308    return properties
309
310def registerResource(pp_up, portal_res, resRegisterFunction, out \
311                    ,RESOURCE_SKIN_LIST, SKIN_RES_REGDATA, UP_PROPERTY, RES_REG_PROPS):
312    """ Register resources in portal's registry, remember existant settings."""
313    # Get original registered resources
314    portal_res_srings = []
315    for r in portal_res.getResources():
316        portal_res_srings.append(";".join(['%%s::%%s'%%(r[0],str(r[1])) \
317                                for r in getResourceProperties(r, RES_REG_PROPS)]))
318    addProperty(pp_up, UP_PROPERTY, portal_res_srings, 'lines', out)
319    # Tune Resource registry according to new skin needs
320    unexistent = [] # list of default resources,
321                    # which present in Skin-product, BUT absent in portal
322    portal_res_ids = portal_res.getResourceIds()
323    for res_dict in SKIN_RES_REGDATA:
324        if res_dict['id'] not in portal_res_ids:
325            # It's interesting - Resource Registry allow adding unexistent resource - use this
326            resRegisterFunction(**res_dict)
327            if res_dict['id'] not in RESOURCE_SKIN_LIST:
328                unexistent.append(res_dict['id'])
329        else:
330            pos = portal_res.getResourcePosition(res_dict['id'])
331            portal_res.unregisterResource(res_dict['id'])
332            resRegisterFunction(**res_dict)
333            portal_res.moveResource(res_dict['id'], pos)
334    if unexistent:
335        print >> out, "!!! - BAD: your Resource Regestry have'nt %%s resource(s), which may lead to some problems." %% unexistent
336
337def getVersion(res_list):
338    """Check version of skin product generator."""
339    return (res_list and not '::' in res_list[0] and '0.5') or '0.7'
340
341def uninstallResource(portal_res, original_res_list, RESOURCE_SKIN_LIST, resRegisterFunction):
342    # Prepare Resource Registry data for backup to original state
343    original_res_regestry = {}
344    genVersion = getVersion(original_res_list)
345    for rec in original_res_list:
346        resource = {}
347        if genVersion == '0.7':
348            [resource.update({prop.split('::')[0]:prop.split('::')[1]}) for prop in rec.split(";")]
349        elif genVersion == '0.5':
350            props = rec.split(";")
351            [resource.update({CSS_BASE_IDS_QPSD053[i]:props[i]}) for i in range(len(CSS_BASE_IDS_QPSD053))]
352        original_res_regestry[resource.pop('id')] = resource
353    # Work up actual Resource Registry
354    res_dict = portal_res.getResourcesDict()
355    for res_id in res_dict.keys():
356        # Remove from Resource Registry Skin product's resources
357        if res_id in RESOURCE_SKIN_LIST \
358           and res_id not in original_res_regestry.keys():
359            portal_res.unregisterResource(res_id)
360            continue
361        # Backup 'enabled' property Registry's resourses to it's original state
362        if original_res_regestry.has_key(res_id):
363            act_Enabled_state = res_dict[res_id].getEnabled()
364            orig_Enabled_state = original_res_regestry[res_id]['enabled']
365            if act_Enabled_state != orig_Enabled_state:
366                pos = portal_res.getResourcePosition(res_id)
367                resource = res_dict[res_id]
368                res = original_res_regestry[res_id]
369                portal_res.unregisterResource(res_id)
370                resRegisterFunction(res_id, **res)
371                portal_res.moveResource(res_id, pos)
372
373def customizeSlots(portal, pp_up, out):
374    # Get original Site's column lists
375    orig_left_slots = left_column = list(portal.left_slots)
376    orig_right_slots = right_column = list(portal.right_slots)
377    # Save original Site's LEFT and RIGHT slots
378    addProperty(pp_up, 'q_left_slots', orig_left_slots, 'lines', out)
379    addProperty(pp_up, 'q_right_slots', orig_right_slots, 'lines', out)
380    # blend-with-site - to portal's slots adding only new one from skin-porduct
381    # blend-with-skin - portal slots forming in the following manner:
382    #                   first adding skin-porduct's slots, than new one from portal
383    # replace - to portal's slots forming only from the skin-porduct's slot list
384    if SLOT_FORMING == "blend_with_skin":
385        left_column, right_column = formSlotsColumn(LEFT_SLOTS, RIGHT_SLOTS, 
386                                                    orig_left_slots, orig_right_slots, MAIN_COLUMN)
387    elif SLOT_FORMING == "blend_with_site":
388        left_column, right_column = formSlotsColumn(orig_left_slots, orig_right_slots,
389                                                    LEFT_SLOTS, RIGHT_SLOTS, MAIN_COLUMN )
390    elif SLOT_FORMING == "replace":
391        left_column, right_column = formSlotsColumn(LEFT_SLOTS, RIGHT_SLOTS, [], [], MAIN_COLUMN)
392    # REPLACE SITE's column slots
393    portal.left_slots = tuple(left_column)
394    portal.right_slots = tuple(right_column)
395    print >> out, "Complited portal slots customization ..."
396
397# main_column ("left" / "right" / "both") mean which of the MAIN column is favour
398def formSlotsColumn(main_left, main_right, slave_left=[], slave_right=[], main_column="both"):
399    result_left = main_left
400    result_right = main_right
401    if main_column == "left":
402    # 1) APPEND to MAIN_LEFT list *new for main_left column* slots from slave_left list
403    # 2) APPEND to MAIN_RIGHT list *new for both main columns* slots from slave_right
404    # 3) REMOVE slots from MAIN_RIGHT list, which are *doubled* in MAIN_LEFT
405        [result_left.append(slot) for slot in slave_left if slot not in result_left]
406        [result_right.append(slot) for slot in slave_right \
407                                   if slot not in result_right and slot not in result_left]
408        [result_right.remove(slot) for slot in result_left if slot in result_right]
409    elif main_column == "right":
410    # 1) APPEND to MAIN_LEFT list *new for main_right column* slots from slave_left list
411    # 2) APPEND to MAIN_RIGHT list *new for both main columns* slots from slave_right
412    # 3) REMOVE slots from MAIN_LEFT list, which are *doubled* in MAIN_RIGHT
413        [result_right.append(slot) for slot in slave_right if slot not in result_right]
414        [result_left.append(slot) for slot in slave_left \
415                                  if slot not in result_left and slot not in result_right]
416        [result_left.remove(slot) for slot in result_right if slot in result_left]
417    elif main_column == "both":
418    # 1) APPEND to MAIN_LEFT list *new for both main columns* slots from slave_left list
419    # 2) APPEND to MAIN_RIGHT list *new for both main columns* slots from slave_right
420        [result_left.append(slot) for slot in slave_left \
421                                  if slot not in result_left and slot not in result_right]
422        [result_right.append(slot) for slot in slave_right \
423                                   if slot not in result_right and slot not in result_left]
424    return [result_left, result_right]
425
426def getProperty(pp, ps, id, default=[]):
427    """ Get property from portal_properties/[property_sheet]"""
428    res = default
429    if ps in pp.objectIds() and pp[ps].hasProperty(id):
430        res = pp[ps].getProperty(id, default)
431    return res
432
433
434###################################
435## OLD INSTALL
436
437def prepareInstallation(portal, pp, out):
438    #uninstallOtherSkinProducts(portal)
439    if not ('uninstall_properties' in pp.objectIds()) :
440        pp.addPropertySheet(id='uninstall_properties', title= 'uninstall_properties')
441        print >> out, "Created 'portal_properties.uninstall_properties' PropertySheet (UP) for backup purpose"
442    return pp.uninstall_properties
443CHECKED_MESSAGE = "The base installation checkings completed."
444
445def uninstallOtherSkinProducts(portal):
446    qi=getToolByName(portal, 'portal_quickinstaller', None)
447    if not qi:
448        raise Exception("Can't work without QuickInstaller tool.")
449    # Get installed products
450    installed_products = [getattr(qi, p_dict['id']) \
451                          for p_dict in qi.listInstalledProducts()
452                          if p_dict['id'] != PRODUCT_NAME]
453    seek_str = "%%s generated product" %% GENERATOR_PRODUCT
454    installed_skin_products = []
455    # Looking for installed skin-products
456    for p in installed_products:
457        transcript = p.getTranscriptAsText()
458        if transcript.find(seek_str) >= 0 :
459            installed_skin_products.append(p.getId())
460    # Uninstall found skin-products
461    if installed_skin_products:
462        qi.uninstallProducts(products=installed_skin_products)
463
464###################################
465## Prepare UNINSTALL
466
467def prepareUninstallSkin(portal, pp_up, out): # ??
468    # Checking for presense SKIN_NAME in portal_skins directory view or among Skin Names
469    skinsTool = getToolByName(portal, 'portal_skins')
470    # Get unique product_skin_name and remember it in case of differ from SKIN_NAME.
471    default_skin = skinsTool.getDefaultSkin()
472    addProperty(pp_up, 'q_default_skin', default_skin, 'string', out)
Note: See TracBrowser for help on using the repository browser.