| 14 | | class IMSBBReader(IMSInterchangeReader): |
| 15 | | """ Read an IMS CC package """ |
| | 12 | def readPackage(self, file, context): |
| | 13 | """ Read the manifest """ |
| | 14 | source = ZipfileReader(file) |
| | 15 | objDict = {} |
| | 16 | if not source: |
| | 17 | return False, 'Internal error. No source object specified' |
| | 18 | bbreader = BBReader() |
| | 19 | manifest = source.readManifest() |
| | 20 | if not manifest: |
| | 21 | raise ManifestError, 'Could not locate manifest file "imsmanifest.xml" in the zip archive.' |
| | 22 | try: |
| | 23 | doc = bbreader.parseManifest(manifest) |
| | 24 | except ExpatError, e: |
| | 25 | raise ManifestError, str(e) |
| | 26 | tocpages = [] |
| | 27 | orgs = bbreader.readOrganizations(doc) |
| | 28 | resources = bbreader.readResources(doc) |
| | 29 | for x in resources: |
| | 30 | resid, restype, bbfile, bbtitle, bbase = bbreader.readResourceAttributes(x) |
| | 31 | if restype == 'resource/x-bb-document': |
| | 32 | metadata = {} |
| | 33 | # read the data file |
| | 34 | if bbfile: |
| | 35 | dataxml = source.readFile(bbfile) |
| | 36 | resnode = bbreader.parseDataFile(dataxml) |
| | 37 | metadata = bbreader.readMetadata(resnode) |
| | 38 | files = bbreader.readFiles(x) |
| | 39 | # If the resource is a file |
| | 40 | if files: |
| | 41 | hash = resid |
| | 42 | objDict[hash] = metadata |
| | 43 | excludeFromNav = True |
| | 44 | file = '%s/%s' %(bbase, files[0]) |
| | 45 | type = self.determineType(objDict[hash], files[0]) |
| | 46 | id = self.createIdFromFile(files[0]) |
| | 47 | path = '%s' %bbase |
| | 48 | if orgs.has_key(resid): |
| | 49 | title = orgs[resid] |
| | 50 | else: |
| | 51 | title = id |
| | 52 | self.applyCoreMetadata(objDict, hash, id, path, excludeFromNav, type, title, file=file) |
| | 53 | # If the resource is a document |
| | 54 | else: |
| | 55 | hash = resid |
| | 56 | objDict[hash] = metadata |
| | 57 | excludeFromNav = True |
| | 58 | type = 'Document' |
| | 59 | id = resid |
| | 60 | path = '' |
| | 61 | if orgs.has_key(resid): |
| | 62 | title = orgs[resid] |
| | 63 | else: |
| | 64 | title = id |
| | 65 | self.applyCoreMetadata(objDict, hash, id, path, excludeFromNav, type, title) |
| | 66 | # The resource is a table of contents page. |
| | 67 | elif restype == 'course/x-bb-coursetoc': |
| | 68 | hash = resid |
| | 69 | objDict[hash] = {} |
| | 70 | excludeFromNav = False |
| | 71 | tocpages.append(resid) |
| | 72 | type = 'Document' |
| | 73 | path = '' |
| | 74 | id = resid |
| | 75 | if orgs.has_key(resid): |
| | 76 | title = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', orgs[resid].split('.')[1]) |
| | 77 | else: |
| | 78 | title = id |
| | 79 | self.applyCoreMetadata(objDict, hash, id, path, excludeFromNav, type, title) |
| 20 | | BBNS = 'http://www.blackboard.com/content-packaging/' |
| 21 | | XML = 'http://www.w3.org/XML/1998/namespace' |
| 22 | | |
| 23 | | def getPackageName(self): |
| 24 | | return self.name |
| 25 | | |
| 26 | | def readPackage(self, context, input): |
| 27 | | self.zf = ZipFile(input, 'r') |
| 28 | | imsdoc = self.zf.read('imsmanifest.xml') |
| 29 | | |
| 30 | | # Read in resources section |
| 31 | | self.tree = ElementTree.XML(imsdoc) |
| 32 | | |
| 33 | | # Enter resource data into a dictionary |
| 34 | | resource_dictionary = self.readResources({}, self.tree) |
| 35 | | |
| 36 | | self.zf.close() |
| 37 | | |
| 38 | | objcreator = getUtility(IIMSObjectCreator) |
| 39 | | objcreator.createObjects(resource_dictionary, context, ZipfileReader(input)) |
| 40 | | |
| 41 | | |
| 42 | | def readResources(self, resDict, tree): |
| 43 | | |
| 44 | | resourceNodes = tree.findall('resources/resource') |
| 45 | | |
| 46 | | for resourceNode in resourceNodes: |
| 47 | | self.readFiles(resDict, resourceNode) |
| 48 | | |
| 49 | | return resDict |
| 50 | | |
| 51 | | def readFiles(self, resDict, resourceNode): |
| 52 | | |
| 53 | | # Get information about the resource. |
| 54 | | # |
| 55 | | res_title = resourceNode.get('{%s}title' %(self.BBNS)) |
| 56 | | # Determining which section it is in (e.g., Assignments, CourseInformation, ExternalLinks...) |
| 57 | | res_title_parts = res_title.split(".") |
| 58 | | if len(res_title_parts) == 4: |
| 59 | | if res_title_parts[0] == 'COURSE_DEFAULT' and res_title_parts[2] == 'CONTENT_LINK' and res_title_parts[3] == 'label': |
| 60 | | # Converts camel case section title (e.g., CourseInformation) to its non-camelcase alternative and sets as title |
| 61 | | res_title = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', res_title_parts[1]).strip(' ') |
| 62 | | # Hash datfile information |
| 63 | | datfile = resourceNode.get('{%s}file' %(self.BBNS)) |
| 64 | | xmlbase = resourceNode.get('{%s}base' %(self.XML)) |
| 65 | | # Hash the blackboard type information for the resource |
| 66 | | bbtype = resourceNode.get('type') |
| 67 | | # Hash the file information |
| 68 | | fileNodes = resourceNode.findall('file') |
| 69 | | id = resourceNode.get('identifier') |
| 70 | | |
| 71 | | |
| 72 | | # Parse dat file |
| 73 | | if datfile: |
| 74 | | datText = self.readContentFile(datfile) |
| 75 | | else: |
| 76 | | datText = '' |
| 77 | | |
| 78 | | |
| 79 | | # Show in navigation? |
| 80 | | item_nodes_all = self.tree.findall("organizations//item") |
| 81 | | targetItems = [node for node in item_nodes_all if node.get("identifierref") == id] |
| 82 | | if targetItems: |
| 83 | | excludeFromNav = False |
| 84 | | # Are there any children? |
| 85 | | child_nodes = targetItems[0].findall('item') |
| 86 | | else: |
| 87 | | excludeFromNav = True |
| 88 | | child_nodes = [] |
| 89 | | |
| 90 | | |
| 91 | | if len(fileNodes): |
| 92 | | # Process resource with file nodes |
| 93 | | files = [] |
| 94 | | for fileNode in fileNodes: |
| 95 | | ## create the file object based on mimetype |
| 96 | | fileid = fileNode.get('href') |
| 97 | | files.append(('%s/%s/view' %(id, fileid),fileid)) |
| 98 | | type = self.determineFileMimetype(fileid) |
| 99 | | resDict[fileid] = {'file':fileid, 'path':id, 'title':fileid, 'id':fileid, 'type':type} |
| 100 | | resText = self.TocPage('Table of Contents', files) |
| 101 | | elif len(child_nodes): |
| 102 | | # Create table of contents pages. |
| 103 | | if not datText: |
| 104 | | items = [] |
| 105 | | for child_node in child_nodes: |
| 106 | | title_nodes = child_node.findall('title') |
| 107 | | if title_nodes: |
| 108 | | title = title_nodes[0].text |
| 109 | | else: |
| 110 | | title = '' |
| 111 | | items.append(('%s.html' %(child_node.get('identifierref')), title)) |
| 112 | | resText = self.TocPage('Table of Contents', items) |
| 113 | | else: |
| 114 | | resText = datText |
| 115 | | elif datText: |
| 116 | | resText = datText |
| 117 | | else: |
| 118 | | resText = '' |
| 119 | | |
| 120 | | resDict[id] = {'text':resText, 'path':'', 'title':res_title, 'id':'%s.html' %id, 'type':'Document'} |
| 121 | | |
| 122 | | |
| 123 | | def TocPage(self, tabletitle, tocitems): |
| 124 | | |
| 125 | | text = '' |
| 126 | | text += '<table class="documentTable" style="width: 499px;" border="0" cellpadding="0" cellspacing="0">' |
| 127 | | text += '<thead>' |
| 128 | | text += ' <tr>' |
| 129 | | text += ' <td>%s</td>' %tabletitle |
| 130 | | text += ' </tr>' |
| 131 | | text += '</thead>' |
| 132 | | text += '<tbody>' |
| 133 | | for tocitem in tocitems: |
| 134 | | text += ' <tr tal:define="oddrow repeat/item/odd;" ' |
| 135 | | text += ' tal:attributes="class oddrow)">' |
| 136 | | text += ' <td ><a href="%s"' %tocitem[0] |
| 137 | | text += ' >%s</a></td>' %tocitem[1] |
| 138 | | text += ' </tr>' |
| 139 | | text += ' </tbody>' |
| 140 | | text += '</table>' |
| 141 | | |
| 142 | | return text |
| 143 | | |
| 144 | | def readContentFile(self, datfile): |
| 145 | | |
| 146 | | # Read the proprietary blackboard.dat file |
| 147 | | datdoc = self.zf.read(datfile) |
| 148 | | |
| 149 | | # Parsing the dat file |
| 150 | | contenttree = ElementTree.XML(datdoc) |
| 151 | | text_nodes = contenttree.findall("BODY/TEXT") |
| 152 | | resText = '' |
| 153 | | |
| 154 | | # Get text information |
| 155 | | if text_nodes: |
| 156 | | resText = text_nodes[0].text |
| 157 | | |
| 158 | | return resText |
| 159 | | |
| 160 | | |