source: Products.ecmigration/trunk/Products/ecmigration/migrate.py @ 893

Revision 893, 35.5 KB checked in by brent, 2 years ago (diff)

Run text fields through safe html filter.

Line 
1# -*- coding: us-ascii -*-
2# _______________________________________________________________________
3#              __________                      .__       
4#   ____   ____\______   \____________  ___  __|__| ______
5# _/ __ \ /    \|     ___/\_  __ \__  \ \  \/  /  |/  ___/
6# \  ___/|   |  \    |     |  | \// __ \_>    <|  |\___ \
7#  \___  >___|  /____|     |__|  (____  /__/\_ \__/____  >
8#      \/     \/                      \/      \/       \/
9# _______________________________________________________________________
10#
11#    This file is part of the eduCommons software package.
12#
13#    Copyright (c) 2011 enPraxis, LLC
14#    http://enpraxis.net
15#
16#    This program is free software; you can redistribute it and/or modify
17#    it under the terms of the GNU General Public License as published by
18#    the Free Software Foundation, version 2.8 
19#
20#    This program is distributed in the hope that it will be useful,
21#    but WITHOUT ANY WARRANTY; without even the implied warranty of
22#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23#    GNU General Public License for more details.
24#
25#    You should have received a copy of the GNU General Public License
26#    along with this program; if not, write to the Free Software
27#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
28# _______________________________________________________________________
29
30__author__ = 'Brent Lambert'
31__version__ = '$ Revision 0.0 $'[11:-2]
32
33
34from Products.CMFPlone.utils import _createObjectByType
35from Products.CMFCore.WorkflowCore import WorkflowException
36from cStringIO import StringIO
37from datetime import datetime
38from urlparse import urlparse, urlunparse
39from BeautifulSoup import BeautifulSoup
40import Globals
41import transaction
42import exceptions
43import cPickle as Pickle
44import Globals
45import os
46import tarfile
47import tempfile
48import logging
49
50
51# Try to import annotation interfaces
52
53try:
54    from collective.contentlicensing.DublinCoreExtensions.interfaces import ILicensable, ILicense
55except ImportError:
56    from Products.ContentLicensing.DublinCoreExtensions.interfaces import ILicensable, ILicense
57
58try:
59    from enpraxis.educommons.annotations.interfaces import IClearCopyrightable, IClearCopyright
60    has_clearcopyrightable = True
61except ImportError:
62    try:
63        from enpraxis.educommons.interfaces import IClearCopyrightable, IClearCopyright
64        has_clearcopyrightable = True
65    except ImportError:
66        has_clearcopyrightable = False
67   
68
69try:
70    from enpraxis.educommons.annotations.interfaces import IAccessibilityCompliantable, IAccessibilityCompliant
71    has_accessibilitycompliance = True
72except ImportError:
73    try:
74        from enpraxis.educommons.interfaces import IAccessibilityCompliantable, IAccessibilityCompliant
75        has_accessibilitycompliance = True
76    except ImportError:
77        has_accessibilitycompliance = False
78
79try:
80    from enpraxis.educommons.annotations.interfaces import ICourseOrderable
81    has_courseorderable = True
82except ImportError:
83    try:
84        from enpraxis.educommons.interfaces import ICourseOrderable
85        has_courseorderable = True
86    except ImportError:
87        has_courseorderable = False
88
89try:
90    from zope.annotation.interfaces import IAnnotations
91    has_annotations = True
92except ImportError:
93    has_annotations = False
94
95
96# Migration logging
97
98def getBaseDirectory(create=True):
99    """ Get the base directory for migration files """
100    try:
101        cwd = Globals.data_dir
102    except AttributeError:
103        cwd = os.getcwd()    # needed for eduCommons 4.0.0
104    basedir = os.path.join(cwd, 'migration')
105    if create:
106        if not os.path.exists(basedir):
107            os.mkdir(basedir)
108    return basedir
109
110def getLogDirectory():
111    getBaseDirectory(create=False)
112    base = getBaseDirectory()
113    return os.path.join(base, 'ecmigration.log')
114
115def setupLogging(logger):
116    """ Set up logging for the migration """
117    logfn = getLogDirectory()
118    handler = logging.FileHandler(logfn)
119    logfmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
120    handler.setFormatter(logfmt)
121    logger.addHandler(handler)
122    logger.setLevel(logging.INFO)
123   
124   
125ecmlogger = logging.getLogger('ecmigration')
126setupLogging(ecmlogger)
127
128
129class MigrationException(exceptions.Exception):
130    """ Migration Error Exception """
131
132    def __init__(self, value):
133        self.value = value
134
135    def __str__(self):
136        return repr(self.value)
137
138
139class FileArchiveManager(object):
140    """ File based archive manager """
141
142    def __init__(self, base, portalid, log):
143        self.base = base
144        portal = os.path.join(self.base, portalid)
145        if not os.path.exists(portal):
146            os.mkdir(portal)
147        self.log = log
148
149    def addFile(self, fn, data):
150        """ Add a file to the archive from a string """
151        self.log.debug(fn)
152        fname = os.path.join(self.base, fn)
153        path = os.path.split(fname)[:-1][0]
154        if not os.path.exists(path):
155            self.mkdir(self.base, fn.split(os.sep)[:-1])
156        f = open(fname, 'wb')
157        f.write(data)
158        f.close()
159
160    def addFileOpen(self, fn):
161        """ Open a file for writing """
162        self.log.debug(fn)
163        fname = os.path.join(self.base, fn)
164        path = os.path.split(fname)[:-1][0]
165        if not os.path.exists(path):
166            self.mkdir(self.base, fn.split(os.sep)[:-1])
167        return open(fname, 'wb')
168
169    def addFileWrite(self, f, data):
170        """ Write data to the file """
171        f.write(data)
172
173    def addFileClose(self, f):
174        """ Close the file """
175        f.close()
176
177    def getFile(self, fn):
178        fname = os.path.join(self.base, fn)
179        try:
180            f = open(fname, 'rb')
181        except IOError:
182            return None
183        data = f.read()
184        f.close()
185        return data
186
187    def getFileOpen(self, fn):
188        fname = os.path.join(self.base, fn)
189        return open(fname, 'rb')
190
191    def getFileClose(self, f):
192        f.close()
193
194    def mkdir(self, path, folders):
195        if folders:
196            p = os.path.join(path, folders[0])
197            fp = os.path.join(self.base, p)
198            if not os.path.exists(fp):
199                os.mkdir(fp)
200            self.mkdir(p, folders[1:])
201                             
202       
203
204
205class TarArchiveManager(object):
206    """ Archive data using tarfile """
207
208    def __init__(self, log, archname, mode='w:bz2'):
209        """ Open the tar file """
210        self.log = log
211        self.tar_archive = tarfile.open(archname, mode)
212
213    def addFileFromString(self, fn, data, lmod):
214        """ Write a file from a string to the archive """
215        tinfo = tarfile.TarInfo(fn)
216        fdata = StringIO(data)
217        fdata.seek(0, 2)
218        tinfo.size = fdata.tell()
219        fdata.seek(0)
220        tinfo.mtime = lmod
221        self.tar_archive.addfile(tinfo, fdata)
222        # Circumvent caching for large file handling
223        self.tar_archive.members = []
224        self.log.debug(fn)
225
226    def addFileFromDisk(self, fn, arcname):
227        """ Write a file from the filesystemto the archive """
228        self.tar_archive.add(fn, arcname)
229        self.log.debug(arcname)
230
231    def getNextFileInfo(self):
232        """ Return file information or None if no files left """
233        return self.tar_archive.next()
234
235    def readFile(self, info):
236        f = self.tar_archive.extractfile(info)
237        if f:
238            data = f.read()
239            f.close()
240            # Circumvent caching for large file handling
241            self.tar_archive.members = []
242            return data
243        return None
244
245    def getFile(self, info):
246        f = self.tar_archive.extractfile(info)
247        self.tar_archive.members = []
248        return f
249
250    def close(self):
251        """ Close the tar file """
252        self.tar_archive.close()
253       
254
255class ECMigration:
256    """ Migrate an eduCommons site """
257
258    transforms = {
259        'ECDepartment':'Division',
260        'ECCourse':'Course',
261        'ECDocument':'Document',
262        'ECFile':'File',
263        'ECFolder':'Folder',
264        'ECImage':'Image',
265        'ECLink':'Link',
266        'GFolder':'Folder',
267        'GDocument':'Document',
268        'GFile':'File',
269        'GImage':'Image',
270        'GLink':'Link',
271        'FSSFile':'File',
272        }
273
274    disallowed_types = [
275        'ECLogFolder',
276        'ECLog'
277        ]
278
279    id_transforms = {
280        'About':'about',
281        'Help':'help',
282        'terms_of_use':'terms-of-use',
283        'privacy_policy':'privacy-policy'
284        }
285
286    state_transforms = {
287        'Visible':'Published',
288        'published':'Published',
289        'Hidden':'InProgress',
290        }
291
292    def __init__(self, context):
293        self.context = context
294        self.pw = context.portal_workflow
295        self.imp = 0
296        self.exp = 0
297        self.base = getBaseDirectory()
298        self.logger = ecmlogger
299        self.crosslistings = []
300
301    def importFilenames(self):
302        portal = self.context.portal_url.getPortalObject()
303        archive = FileArchiveManager(self.base, portal.getId(), self.logger)
304        filelistfn = os.path.join(portal.getId(), 'filelist.ecmigration')
305        files = self._getMetadata(filelistfn, archive)
306        for x in files:
307            ext = x.split('.')[-1]
308            if 'ecmigration_metadata' == ext:
309                data = self._getMetadata(x, archive)
310                if data['type'] in ['File', 'Image']:
311                    if data.has_key('originfn'):
312                        oid = x.split('.' + ext)[0]
313                        oid = '/'.join(oid.split(os.sep))
314                        obj = self._getObjectByPath(oid)
315                        if obj:
316                            if not obj.getFilename():
317                                obj.setFilename(data['originfn'])
318
319    def importContent(self):
320        """ Import a migration """
321        self.crosslistings = []
322        self.logger.info('Starting Import')
323        starttime = datetime.now()
324        portal = self.context.portal_url.getPortalObject()
325        archive = FileArchiveManager(self.base, portal.getId(), self.logger)
326        filelistfn = os.path.join(portal.getId(), 'filelist.ecmigration')
327        self.logger.info('Getting file list: %s' %filelistfn)
328        files = self._getMetadata(filelistfn, archive)
329        self.imp = 0
330        for x in files:
331            ext = x.split('.')[-1]
332            if 'ecmigration_filelist' == ext:
333                pass
334            elif 'ecmigration_settings' == ext:
335                self.logger.info('Getting eduCommons settings: %s' %x)
336                data = self._getMetadata(x, archive)
337                self.importSettings(data)
338            elif 'ecmigration_theme' == ext:
339                self.logger.info('Getting eduCommons theme settings: %s' %x)
340                data = self._getMetadata(x, archive)
341                self.importThemeSettings(data)
342                self.importThemeFiles(archive)
343            elif 'ecmigration_userinfo' == ext:
344                self.logger.info('Getting eduCommons user info: %s' %x)
345                data = self._getMetadata(x, archive)
346                self.importUsers(data)
347            elif 'ecmigration_directory' == ext:
348                data = self._getMetadata(x, archive)
349                if data['type'] not in self.disallowed_types:
350                    obj = self.importObject(data)
351                    self.imp += 1
352            elif 'ecmigration_metadata' == ext:
353                data = self._getMetadata(x, archive)
354                if data['type'] not in self.disallowed_types:
355                    obj = self.importObject(data)
356            else:
357                self.importObjectData(x, archive)
358                self.imp += 1
359        self.setCrosslistings()
360        endtime = datetime.now()
361        self.logger.info('Imported %d objects' %self.imp)
362        self.logger.info('Total Time: %s' %(endtime-starttime))
363
364
365    def importSettings(self, data):
366        """ Import Settings """
367        portal = self.context.portal_url.getPortalObject()
368        props = portal.portal_properties.site_properties
369        props.manage_changeProperties(default_language=data['default_language'])
370        transaction.savepoint(optimistic=True)
371
372    def importThemeSettings(self, data):
373        """ Import theme data """
374        portal = self.context.portal_url.getPortalObject()
375        props = portal.portal_skins.custom
376        bp = props.base_properties
377        bp.manage_changeProperties(
378            logoName=data['logoName'],
379            faviconName=data['faviconName'],
380            bodyBackgroundColor=data['bodyBackgroundColor'],
381            bodyBackgroundImage=data['bodyBackgroundImage'],
382            portalHeaderBackgroundImage=data['portalHeaderBackgroundImage'],
383            portalHeaderBackgroundColor=data['portalHeaderBackgroundColor'],
384            portalHeaderFontColor=data['portalHeaderFontColor'],
385            portalHeaderLinkColor=data['portalHeaderLinkColor'],
386            portalHeaderActiveColor=data['portalHeaderActiveColor'],
387            portalTopNavBackgroundImage=data['portalTopNavBackgroundImage'],
388            portalTopNavBackgroundColor=data['portalTopNavBackgroundColor'],
389            portalTopNavFontColor=data['portalTopNavFontColor'],
390            portalTopNavLinkColor=data['portalTopNavLinkColor'],
391            portalTopNavActiveColor=data['portalTopNavActiveColor'],
392            portalColumnOneBackgroundImage=data['portalColumnOneBackgroundImage'],
393            portalColumnOneBackgroundColor=data['portalColumnOneBackgroundColor'],
394            portalColumnOneFontColor=data['portalColumnOneFontColor'],
395            portalColumnOneLinkColor=data['portalColumnOneLinkColor'],
396            portalColumnOneActiveColor=data['portalColumnOneActiveColor'],
397            backgroundColor=data['backgroundColor'],
398            fontColor=data['fontColor'],
399            linkColor=data['linkColor'],
400            activeColor=data['activeColor'],
401            )
402        transaction.savepoint(optimistic=True)
403
404    def importThemeFiles(self, archive):
405        portal = self.context.portal_url.getPortalObject()
406        props = portal.portal_skins.custom
407        bp = props.base_properties
408        images = [
409            bp.getProperty('logoName'),
410            bp.getProperty('faviconName'),
411            bp.getProperty('bodyBackgroundImage'),
412            bp.getProperty('portalHeaderBackgroundImage'),
413            bp.getProperty('portalTopNavBackgroundImage'),
414            bp.getProperty('portalColumnOneBackgroundImage'),
415            ]
416        path = '%s/portal_skins/custom/' %self.context.getId()
417        for x in images:
418            data = archive.getFile(path+x)
419            if data:
420                if x in props.objectIds():
421                    props.manage_delObjects(ids=[x])
422                props.manage_addImage(x, data)
423                self.logger.info('Added Theme File: %s' %path+x)
424        transaction.savepoint(optimistic=True)
425               
426    def importUsers(self, data):
427        """ Import Users """
428        users = data['users']
429        portal = self.context.portal_url.getPortalObject()
430        pr = portal.portal_registration
431        for x in users:
432            if not users[x]['email']:
433                users[x]['email'] = 'a@b.com'
434            pr.addMember(id=x,
435                         password=pr.generatePassword(),
436                         roles=users[x]['roles'],
437                         properties = {'fullname':users[x]['fullname'],
438                                       'username':x,
439                                       'email':users[x]['email']})
440        for x in users:
441            portal.acl_users.source_users._user_passwords[x] = users[x]['password']
442        transaction.savepoint(optimistic=True)
443
444    def importObject(self, data):
445        """ Import an object from data out of the archive """
446        oid = '/'.join(data['filename'].split(os.sep))
447        obj = self._getObjectByPath(oid)
448        if not obj:
449            # Object does not exist, create it
450            parent = self._getObjectByPath('/'.join(oid.split('/')[:-1]))
451            if parent:
452                nid = oid.split('/')[-1]
453                if 'FSSFile' == data['type']:
454                    dt = 'File'
455                else:
456                    dt = data['type']
457                _createObjectByType(dt, parent, id=nid)
458                obj = getattr(parent, nid)
459            else:
460                pass
461        if obj:
462            if getattr(obj, 'getId', None):
463                if obj.portal_type == data['type']:
464                    self._updateObject(obj, data)
465                else:
466                    obj = None
467        self.logger.info('I%.9d %s' %(self.imp, oid))
468        return obj
469
470    def importObjectData(self, fn, archive):
471        """ Import file data into the object. """
472        oid = '/'.join(fn.split(os.sep))
473        obj = self._getObjectByPath(oid)
474        if obj:
475            if 'File' == obj.Type():
476                f = archive.getFileOpen(fn)
477                fobj = obj.getFile()
478                try:
479                    fobj.manage_upload(f)
480                except AttributeError, e:
481                    obj.setFile(f)
482                fobj.content_type = obj.content_type
483                archive.getFileClose(f)
484            else:
485                data = archive.getFile(fn)
486                pf = obj.getPrimaryField()
487                pf.set(obj, data)
488            obj.reindexObject()
489            transaction.savepoint(optimistic=True)
490        else:
491            pass
492
493    def _updateObject(self, obj, data):
494        """ Update the object with the new settings """
495        for x in data['fields']:
496            field = obj.getField(x)
497            if field:
498                if 'crosslisting' == field.getName():
499                    self.crosslistings.append((obj, data['fields'][x]))
500                else:
501                    field.set(obj, data['fields'][x])
502        if data.has_key('rightsholder'):
503            if ILicensable.providedBy(obj):
504                lic = ILicense(obj)
505                lic.setRightsHolder(data['rightsholder'])
506                lic.setRightsLicense(data['rightslicense'])
507        if data.has_key('clearedcopyright'):
508            if IClearCopyrightable.providedBy(obj):
509                cc = IClearCopyright(obj)
510                cc.setClearedCopyright(data['clearedcopyright'])
511        if data.has_key('accessibilitycompliant'):
512            if IAccessibilityCompliantable.providedBy(obj):
513                acc = IAccessibilityCompliant(obj)
514                if getattr(acc, 'setAccessible', None):
515                    acc.setAccessible(data['accessibilitycompliant'])
516                else:
517                    # Function name changed in eduCommons 3.2.1+
518                    acc.setAccessibilityCompliant(data['accessibilitycompliant'])
519        if data.has_key('courseorder'):
520            if ICourseOrderable.providedBy(obj):
521                try:
522                    from enpraxis.educommons.annotations.interfaces import ICourseOrder
523                    order = ICourseOrder(obj)
524                except ImportError, NameError:
525                    if has_annotations:
526                        ann = IAnnotations(obj)
527                        if 'Course' == data['type']:
528                            ann['eduCommons.objPositionInCourse'] = 0
529                        else:
530                            ann['eduCommons.objPositionInCourse'] = data['courseorder'] + 1
531                else:
532                    if 'Course' == data['type']:
533                        order.setPositionInCourse(0)
534                    else:
535                        order.setPositionInCourse(data['courseorder'] + 1)
536        if data.has_key('originfn'):
537            obj.setFilename(data['originfn'])
538        if data.has_key('content_type'):
539            obj.content_type = data['content_type']
540        try:
541            obj.reindexObject()
542        except AttributeError:
543            pass
544        transaction.savepoint(optimistic=True)
545        self._setWorkflow(obj, data['review_state'])
546
547    def _getMetadata(self, fn, archive):
548        """ Unpickle metadata and return it. """
549        data = archive.getFile(fn)
550        return Pickle.loads(data)
551
552    def _getObjectByPath(self, path):
553        """ Return an object via its path """
554        opath = path.split('/')[1:]
555        obj = self.context
556        for x in opath:
557            if getattr(obj.aq_base, x, None):
558                obj = obj[x]
559            else:
560                obj = None
561                break
562        return obj
563   
564    def _setWorkflow(self, obj, state):
565        pw = obj.portal_workflow
566        wf = pw.getWorkflowsFor(obj)[0]
567        if 'content_workflow' == wf.getId():
568            if state in wf.states:
569                if state != pw.getInfoFor(obj, 'review_state'):
570                    pw.doActionFor(obj, 'submit')
571                if state != pw.getInfoFor(obj, 'review_state'):
572                    pw.doActionFor(obj, 'release')
573                if state != pw.getInfoFor(obj, 'review_state'):
574                    pw.doActionFor(obj, 'publish')
575
576    def setCrosslistings(self):
577        """ Set up cross listings """
578        portal = self.context.portal_url.getPortalObject()
579        for x in self.crosslistings:
580            obj = x[0]
581            field = obj.getField('crosslisting')
582            uids = []
583            for y in x[1]:
584                robj = getattr(portal, y, None)
585                if robj:
586                    uids.append(robj.UID())
587            if uids:
588                field.set(obj, uids)
589                   
590
591    def exportContent(self, filename=None):
592        """ Export eduCommons content out of site ready for migration. """
593        # Set starting params
594        starttime = datetime.now()
595        portal = self.context.portal_url.getPortalObject()
596        archive = FileArchiveManager(self.base, portal.getId(), self.logger)
597        brains = self.context.portal_catalog(path={'query':'/', 'depth':2,},)
598        self.exp = 0
599        self.fixed = 0
600        self.files = []
601        # Do the export
602        self.logger.info('Starting Export')
603        self.exportSettings(archive)
604        self.exportThemeSettings(archive)
605        self.exportUsers(archive)
606        self.exportObjects(brains, archive, 99999)
607        data = Pickle.dumps(self.files)
608        flist = os.path.join(portal.getId(), 'filelist.ecmigration')
609        archive.addFile(flist, data)
610        self.logger.info('Added file info list: %s' %flist)
611        # Record results
612        endtime = datetime.now()
613        self.logger.info('Exported %d objects' %self.exp)
614        self.logger.info('Fixed %d objects' %self.fixed)
615        self.logger.info('Total time %s' %(endtime-starttime))
616
617    def exportSettings(self, archive):
618        props = self.context.portal_properties.site_properties
619        fn = '%s/settings.ecmigration_settings' %self.context.getId()
620        objstore = {'filename':fn, 'type':'Settings_ecmigration',}
621        objstore['default_language'] = props.getProperty('default_language')
622        data = Pickle.dumps(objstore)
623        self.files.append(fn)
624        archive.addFile(fn, data)
625        self.logger.info('Added settings file: %s' %fn)
626
627    def exportThemeSettings(self, archive):
628        tprops = self.context.portal_skins.custom
629        bp = tprops.base_properties
630        fn = '%s/theme.ecmigration_theme' %self.context.getId()
631        objstore = {
632            'logoName':bp.getProperty('logoName'),
633            'faviconName':bp.getProperty('faviconName'),
634            'bodyBackgroundColor':bp.getProperty('bodyBackgroundColor'),
635            'bodyBackgroundImage':bp.getProperty('bodyBackgroundImage'),
636            'portalHeaderBackgroundImage':bp.getProperty('portalHeaderBackgroundImage'),
637            'portalHeaderBackgroundColor':bp.getProperty('portalHeaderBackgroundColor'),
638            'portalHeaderFontColor':bp.getProperty('portalHeaderFontColor'),
639            'portalHeaderLinkColor':bp.getProperty('portalHeaderLinkColor'),
640            'portalHeaderActiveColor':bp.getProperty('portalHeaderActiveColor'),
641            'portalTopNavBackgroundImage':bp.getProperty('portalTopNavBackgroundImage'),
642            'portalTopNavBackgroundColor':bp.getProperty('portalTopNavBackgroundColor'),
643            'portalTopNavFontColor':bp.getProperty('portalTopNavFontColor'),
644            'portalTopNavLinkColor':bp.getProperty('portalTopNavLinkColor'),
645            'portalTopNavActiveColor':bp.getProperty('portalTopNavActiveColor'),
646            'portalColumnOneBackgroundImage':bp.getProperty('portalColumnOneBackgroundImage'),
647            'portalColumnOneBackgroundColor':bp.getProperty('portalColumnOneBackgroundColor'),
648            'portalColumnOneFontColor':bp.getProperty('portalColumnOneFontColor'),
649            'portalColumnOneLinkColor':bp.getProperty('portalColumnOneLinkColor'),
650            'portalColumnOneActiveColor':bp.getProperty('portalColumnOneActiveColor'),
651            'backgroundColor':bp.getProperty('backgroundColor'),
652            'fontColor':bp.getProperty('fontColor'),
653            'linkColor':bp.getProperty('linkColor'),
654            'activeColor':bp.getProperty('activeColor'),
655            }
656        data = Pickle.dumps(objstore)
657        self.files.append(fn)
658        archive.addFile(fn, data)
659        self.logger.info('Added theme settings: %s' %fn)
660        images = [
661            objstore['logoName'],
662            objstore['faviconName'],
663            objstore['bodyBackgroundImage'],
664            objstore['portalHeaderBackgroundImage'],
665            objstore['portalTopNavBackgroundImage'],
666            objstore['portalColumnOneBackgroundImage'],
667            ]
668        path = '%s/portal_skins/custom/' %self.context.getId()
669        for x in images:
670            if x in tprops.objectIds():
671                img = getattr(tprops, x, None)
672                if img:
673                    archive.addFile(path + x, img.data)
674                    self.logger.info('Added theme image file: %s' %path+x)
675
676    def exportUsers(self, archive):
677        fn = '%s/userinfo.ecmigration_userinfo' %self.context.getId()
678        objstore = {'filename':fn, 'type':'UserInfo_ecmigration', 'users':{},}
679        for user in self.context.acl_users.getUsers():
680            username = user.getName()
681            password = self.context.acl_users.source_users._user_passwords[username]
682            objstore['users'][username] = {
683                'password':password,
684                'fullname':user.getProperty('fullname'),
685                'email':user.getProperty('email'),
686                'roles':user.getRoles(),
687                }
688        data = Pickle.dumps(objstore)
689        self.files.append(fn)
690        archive.addFile(fn, data)
691        self.logger.info('Added user info file: %s' %fn)
692
693    def exportObjects(self, brains, tf, depth=0):
694        for x in brains:
695            if x.getId not in ['Feedback', 'feedback', 'Courses_listing', 'courselist']:
696                self.exportObject(x, tf)
697                if depth and x.is_folderish:
698                    brains = self.context.portal_catalog(path={'query':x.getPath(), 'depth':1,},)
699                    self.exportObjects(brains, tf, depth-1)
700
701    def exportObject(self, brain, archive):
702        """ Export an object """
703        if 'Download this Course' == brain.Title:
704            return
705        fn = self._getPath(brain)
706        obj = brain.getObject()
707        if obj.portal_type not in ['ECLogFolder', 'ECLog']:
708            # Get the metadata and store it
709            if obj.isPrincipiaFolderish:
710                self.exportMetadata(obj, fn, '.ecmigration_directory', archive)
711            else:
712                self.exportMetadata(obj, fn, '.ecmigration_metadata', archive)
713                self.exportFile(obj, fn, archive)
714        obj._p_deactivate()
715        transaction.savepoint()
716        minimize = self.context.restrictedTraverse('/Control_Panel/Database/main/manage_minimize')
717        minimize()
718        self.logger.info('E%.9d %s' %(self.exp, fn))
719        self.exp += 1
720       
721    def exportMetadata(self, obj, fn, ftype, archive):
722        """ Export an object's metadata """
723        objstore = {'filename':fn}
724        self._storeMetaData(obj, objstore)
725        data = Pickle.dumps(objstore)
726        self.files.append(fn+ftype)
727        archive.addFile(fn+ftype, data)
728
729    def exportFile(self, obj, fn, archive):
730        """ Export the primary field of an object as a file in the archive """
731        pfield = obj.getPrimaryField()
732        fd = pfield.get(obj)
733        if 'text' == pfield.getName():
734            # Convert ResolveUids to real links
735            try:
736                ds = self.context.portal_transforms.convert('fck_ruid_to_url', fd, context=obj)
737            except:
738                # Transform no longer in eduCommons 3.2.1+
739                ds = self.context.portal_transforms.convertTo(
740                    'text/x-html-safe',
741                    fd,
742                    context=self.context,
743                    encoding='utf8')
744            if ds:
745                data = ds.getData()
746            else:
747                data = fd
748            data = self._transformLinks(data)
749            self.files.append(fn)
750            archive.addFile(fn, data)
751        elif type(fd) == type(''):
752            data = fd
753            self.files.append(fn)
754            archive.addFile(fn, data)
755        else:
756            data = fd.data
757            if type(data) != type(''):
758                self.files.append(fn)
759                f = archive.addFileOpen(fn)
760                while data is not None:
761                    archive.addFileWrite(f, data.data)
762                    data = data.next
763                archive.addFileClose(f)   
764            else:
765                self.files.append(fn)
766                archive.addFile(fn, data)
767               
768    def _getPath(self, brain):
769        opath = brain.getPath()
770        fn = opath.split('/')
771        self._transformId(fn)
772        return os.sep.join(fn[1:])
773
774    def _storeMetaData(self, obj, objstore):
775        """ Export Object Metadata """
776        objstore['type'] = self._transformType(obj.portal_type)
777        # Get field data
778        pfield = obj.getPrimaryField()
779        if objstore['type'] in ['School', 'Division', 'Course']:
780            pfield = None
781        objstore['fields'] = {}
782        for x in obj.Schema().fields():
783            if x != pfield:
784                fid = x.getName()
785                if fid in ['id', 'locallyAllowedTypes', 'immediatelyAddableTypes', 'constrainTypesMode', ]:
786                    pass # Do not store the ID, get it from the filename instead
787                elif 'clearedCopyright' == fid:
788                    # if clearedcopyright flag is stored as a field
789                    # move it to annotations instead
790                    objstore['clearedcopyright'] = x.get(obj)
791                elif 'navPosition' == fid:
792                    objstore['courseorder'] = x.get(obj)
793                elif fid in ['file', 'image']:
794                    pass
795                elif 'text' == fid:
796                    # If html then remove all resolveUIDs
797                    data = x.get(obj)
798                    try:
799                        ds = self.context.portal_transforms.convert('fck_ruid_to_url', data, context=obj)
800                    except:
801                        # Transform is not in eduCommons 3.2.1+
802                        ds = self.context.portal_transforms.convertTo(
803                            'text/x-html-safe',
804                            data,
805                            context=obj,
806                            encoding='utf-8')
807                    if ds:
808                        data = ds.getData()
809                    data = self._transformLinks(data)
810                    objstore['fields'][fid] = data
811                elif 'crosslisting' == fid:
812                    refobjs = x.get(obj)
813                    if refobjs:
814                        robjs = []
815                        for y in refobjs:
816                            if y != 'None':
817                                robjs.append(y)
818                        objstore['fields'][fid] = tuple(robjs)
819                    else:
820                        objstore['fields'][fid] = ()   
821                else:
822                    objstore['fields'][fid] = x.get(obj)
823        # Get non field related data
824        objstore['owner'] = obj.getOwner().getId()
825        objstore['workflowhistory'] = obj.workflow_history
826        objstore['review_state'] = self._transformState(obj.portal_workflow.getInfoFor(obj, 'review_state'))
827        #print objstore['review_state']
828        # Get annotation related data
829        if ILicensable.providedBy(obj):
830            lic = ILicense(obj)
831            objstore['rightsholder'] = lic.getRightsHolder()
832            objstore['rightslicense'] = lic.getRightsLicense()
833        if has_clearcopyrightable:
834            if IClearCopyrightable.providedBy(obj):
835                cc = IClearCopyright(obj)
836                objstore['clearedcopyright'] = cc.getClearedCopyright()
837        if has_accessibilitycompliance:
838            if IAccessibilityCompliantable.providedBy(obj):
839                acc = IAccessibilityCompliant(obj)
840                if getattr(acc, 'getAccessible', None):
841                    objstore['accessibilitycompliant'] = acc.getAccessible()
842                else:
843                    objstore['accessibilitycompliant'] = acc.getAccessibilityCompliant()
844        if has_courseorderable:
845            if ICourseOrderable.providedBy(obj):
846                try:
847                    from enpraxis.educommons.annotations.interfaces import ICourseOrder
848                except ImportError:
849                    from zope.annotation.interfaces import IAnnotations
850                    ann = IAnnotations(obj)
851                    if ann.has_key('eduCommons.objPositionInCourse'):
852                        co = ann['eduCommons.objPositionInCourse']
853                    else:
854                        co = 0
855                else:
856                    order = ICourseOrder(obj)
857                    co = order.getPositionInCourse()
858                if co:
859                    objstore['courseorder'] = co
860        if getattr(obj, 'content_type', None):
861            objstore['content_type'] = obj.content_type
862        if obj.Type() in ['File', 'Image']:
863            objstore['originfn'] = obj.getFilename()
864
865    def _transformType(self, ptype):
866        """ Change type """
867        if self.transforms.has_key(ptype):
868            return self.transforms[ptype]
869        else:
870            return ptype
871
872    def _transformId(self, fn):
873        """ Change IDs """
874        if len(fn) > 2 and self.id_transforms.has_key(fn[2]):
875            fn[2] = self.id_transforms[fn[2]]
876            if len(fn) > 3 and 'about' == fn[2] and 'index_html' == fn[3]:
877                fn[3] = 'abouttext_text'
878            if len(fn) > 3 and 'help' == fn[2] and 'index_html' == fn[3]:
879                fn[3] = 'help_text'
880        if len(fn) > 3 and self.id_transforms.has_key(fn[3]):
881            fn[3] = self.id_transforms[fn[3]]
882
883    def _transformState(self, state):
884        if state in self.state_transforms:
885            state = self.state_transforms[state]
886        return state
887
888    def _transformLinks(self, data):
889        soup = BeautifulSoup(data)
890        links = soup.findAll(href=True)
891        for x in links:
892            href = x['href']
893            url = self._transformLinkUrl(href)
894            if url:
895                x['href'] = url
896        images = soup.findAll(src=True)
897        for x in images:
898            src = x['src']
899            url = self._transformLinkUrl(src)
900            if url:
901                x['src'] = url
902        return soup.renderContents()
903
904    def _transformLinkUrl(self, url):
905        """ Fix bad urls """
906        purl = urlparse(url)
907        if '' == purl[0] and '' == purl[1]:
908            if '/editor/' in purl[2]:
909                purl = (purl[0], purl[1], purl[2].replace('/editor/', '/'), purl[3], purl[4], purl[5])
910            if len(purl[2]) > 0 and '/' == purl[2][0]:
911                nurl = purl[2].split('/')
912                if len(nurl) > 1 and self.context.getId() != nurl[1]:
913                    nurl.insert(1, self.context.getId())
914                    purl = (purl[0], purl[1], '/'.join(nurl), purl[3], purl[4], purl[5])
915        newurl = urlunparse(purl)
916        if newurl != url:
917            self.fixed += 1
918            return newurl
919   
920        else:
921            return None
922                   
Note: See TracBrowser for help on using the repository browser.