# ===========================================================================
# eXe
# Copyright 2004-2005, University of Auckland
# Copyright 2006-2009 eXe Project, http://eXeLearning.org/
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# ===========================================================================
"""
This is the main XUL page.
"""
import os
import sys
import logging
import traceback
import shutil
from xml.sax.saxutils import escape
from twisted.internet import reactor
from twisted.web import static
from twisted.internet.defer import Deferred
from nevow import loaders, inevow, stan
from nevow.livepage import handler, js
from exe.xului.idevicepane import IdevicePane
from exe.xului.outlinepane import OutlinePane
from exe.xului.stylemenu import StyleMenu
from exe.webui.renderable import RenderableLivePage
from exe.xului.propertiespage import PropertiesPage
from exe.webui.authoringpage import AuthoringPage
from exe.export.websiteexport import WebsiteExport
from exe.export.textexport import TextExport
from exe.export.singlepageexport import SinglePageExport
from exe.export.scormexport import ScormExport
from exe.export.imsexport import IMSExport
from exe.export.ipodexport import IpodExport
from exe.engine.path import Path, toUnicode
from exe.engine.package import Package
from exe import globals as G
from tempfile import mkdtemp
from exe.engine.mimetex import compile
log = logging.getLogger(__name__)
class MainPage(RenderableLivePage):
"""
This is the main XUL page. Responsible for handling URLs.
"""
_templateFileName = 'mainpage.xul'
name = 'to_be_defined'
def __init__(self, parent, package):
"""
Initialize a new XUL page
'package' is the package that we look after
"""
self.name = package.name
RenderableLivePage.__init__(self, parent, package)
self.putChild("resources", static.File(package.resourceDir))
mainxul = Path(self.config.xulDir).joinpath('templates', 'mainpage.xul')
self.docFactory = loaders.xmlfile(mainxul)
# Create all the children on the left
self.outlinePane = OutlinePane(self)
self.idevicePane = IdevicePane(self)
self.styleMenu = StyleMenu(self)
# And in the main section
self.authoringPage = AuthoringPage(self)
self.propertiesPage = PropertiesPage(self)
# translate the "don't close the window" message
red_x = _("Please use eXe's\n File... Quit\nmenu to close eXe.")
def getChild(self, name, request):
"""
Try and find the child for the name given
"""
if name == '':
return self
else:
return super(self, self.__class__).getChild(self, name, request)
def goingLive(self, ctx, client):
"""Called each time the page is served/refreshed"""
inevow.IRequest(ctx).setHeader('content-type', 'application/vnd.mozilla.xul+xml')
# Set up named server side funcs that js can call
def setUpHandler(func, name, *args, **kwargs):
"""
Convience function link funcs to hander ids
and store them
"""
kwargs['identifier'] = name
hndlr = handler(func, *args, **kwargs)
hndlr(ctx, client) # Stores it
setUpHandler(self.handleIsPackageDirty, 'isPackageDirty')
setUpHandler(self.handlePackageFileName, 'getPackageFileName')
setUpHandler(self.handleSavePackage, 'savePackage')
setUpHandler(self.handleLoadPackage, 'loadPackage')
setUpHandler(self.handleLoadRecent, 'loadRecent')
setUpHandler(self.handleLoadTutorial, 'loadTutorial')
setUpHandler(self.handleClearRecent, 'clearRecent')
setUpHandler(self.handleExport, 'exportPackage')
setUpHandler(self.handleQuit, 'quit')
setUpHandler(self.handleBrowseURL, 'browseURL')
setUpHandler(self.handleInsertPackage, 'insertPackage')
setUpHandler(self.handleExtractPackage, 'extractPackage')
setUpHandler(self.outlinePane.handleSetTreeSelection,
'setTreeSelection')
setUpHandler(self.handleClearAndMakeTempPrintDir,
'makeTempPrintDir')
setUpHandler(self.handleRemoveTempDir, 'removeTempDir')
setUpHandler(self.handleTinyMCEimageChoice, 'previewTinyMCEimage')
setUpHandler(self.handleTinyMCEmath, 'generateTinyMCEmath')
setUpHandler(self.handleTestPrintMsg, 'testPrintMessage')
setUpHandler(self.handleSetLocale, 'setLocale')
setUpHandler(self.handleSetInternalAnchors, 'setInternalAnchors')
self.idevicePane.client = client
# Render the js
handleId = "'", client.handleId, "'"
def render_mainMenu(self, ctx, data):
"""Mac menubars are not shown
so make it a toolbar"""
if sys.platform[:6] == "darwin":
ctx.tag.tagName = 'toolbar'
return ctx.tag
def render_addChild(self, ctx, data):
"""Fills in the oncommand handler for the
add child button and short cut key"""
return ctx.tag(oncommand=handler(self.outlinePane.handleAddChild,
js('currentOutlineId()')))
def render_delNode(self, ctx, data):
"""Fills in the oncommand handler for the
delete child button and short cut key"""
return ctx.tag(oncommand=handler(self.outlinePane.handleDelNode,
js("confirmDelete()"),
js('currentOutlineId()')))
def render_renNode(self, ctx, data):
"""Fills in the oncommand handler for the
rename node button and short cut key"""
return ctx.tag(oncommand=handler(self.outlinePane.handleRenNode,
js('currentOutlineId()'),
js('askNodeName()'), bubble=True))
def render_prePath(self, ctx, data):
"""Fills in the package name to certain urls in the xul"""
request = inevow.IRequest(ctx)
return ctx.tag(src=self.package.name + '/' + ctx.tag.attributes['src'])
# The node moving buttons
def _passHandle(self, ctx, name):
"""Ties up a handler for the promote, demote,
up and down buttons. (Called by below funcs)"""
attr = getattr(self.outlinePane, 'handle%s' % name)
return ctx.tag(oncommand=handler(attr, js('currentOutlineId()')))
def render_promote(self, ctx, data):
"""Fills in the oncommand handler for the
Promote button and shortcut key"""
return self._passHandle(ctx, 'Promote')
def render_demote(self, ctx, data):
"""Fills in the oncommand handler for the
Demote button and shortcut key"""
return self._passHandle(ctx, 'Demote')
def render_up(self, ctx, data):
"""Fills in the oncommand handler for the
Up button and shortcut key"""
return self._passHandle(ctx, 'Up')
def render_down(self, ctx, data):
"""Fills in the oncommand handler for the
Down button and shortcut key"""
return self._passHandle(ctx, 'Down')
def render_recentProjects(self, ctx, data):
"""
Fills in the list of recent projects menu
"""
result = ['
\n''')
try:
news = open(os.path.join(self.config.webDir, 'NEWS'),
'rt').read()
readme = open(os.path.join(self.config.webDir, 'README'),
'rt').read()
f.write(news)
f.write('\n')
f.write(readme)
except IOError:
# fail silently if we can't read either of the files
pass
f.write('')
f.close()
url = url.replace('%t', release_notes)
else:
url = url.replace('%s', self.config.webDir)
log.debug(u'browseURL: ' + url)
if hasattr(os, 'startfile'):
os.startfile(url)
elif sys.platform[:6] == "darwin":
import webbrowser
webbrowser.open(url, new=True)
else:
os.system("firefox " + url + "&")
def handleInsertPackage(self, client, filename):
"""
Load the package and insert in current node
"""
loadedPackage = self._loadPackage(client, filename, newLoad=False,
destinationPackage=self.package)
newNode = loadedPackage.root.copyToPackage(self.package,
self.package.currentNode)
# trigger a rename of all of the internal nodes and links,
# and to add any such anchors into the dest package via isMerge:
newNode.RenamedNodePath(isMerge=True)
client.sendScript((u'top.location = "/%s"' % \
self.package.name).encode('utf8'))
def handleExtractPackage(self, client, filename, existOk):
"""
Create a new package consisting of the current node and export
'existOk' means the user has been informed of existance and ok'd it
"""
filename = Path(filename)
saveDir = filename.dirname()
if saveDir and not saveDir.exists():
client.alert(_(u'Cannot access directory named ') + unicode(saveDir) + _(u'. Please use ASCII names.'))
return
# Add the extension if its not already there
if not filename.lower().endswith('.elp'):
filename += '.elp'
if Path(filename).exists() and existOk != 'true':
msg = _(u'"%s" already exists.\nPlease try again with a different filename') % filename
client.alert(_(u'EXTRACT FAILED!\n%s' % msg))
return
try:
# Create a new package for the extracted nodes
newPackage = self.package.extractNode()
# trigger a rename of all of the internal nodes and links,
# and to remove any old anchors from the dest package,
# and remove any zombie links via isExtract:
newNode = newPackage.root
if newNode:
newNode.RenamedNodePath(isExtract=True)
# Save the new package
newPackage.save(filename)
except Exception, e:
client.alert(_('EXTRACT FAILED!\n%s' % str(e)))
raise
client.alert(_(u'Package extracted to: %s' % filename))
# Public Methods
def exportSinglePage(self, client, filename, webDir, stylesDir, \
printFlag):
"""
Export 'client' to a single web page,
'webDir' is just read from config.webDir
'stylesDir' is where to copy the style sheet information from
'printFlag' indicates whether or not this is for print
(and whatever else that might mean)
"""
try:
imagesDir = webDir.joinpath('images')
scriptsDir = webDir.joinpath('scripts')
templatesDir = webDir.joinpath('templates')
# filename is a directory where we will export the website to
# We assume that the user knows what they are doing
# and don't check if the directory is already full or not
# and we just overwrite what's already there
filename = Path(filename)
# Append the package name to the folder path if necessary
if filename.basename() != self.package.name:
filename /= self.package.name
if not filename.exists():
filename.makedirs()
elif not filename.isdir():
client.alert(_(u'Filename %s is a file, cannot replace it') %
filename)
log.error("Couldn't export web page: "+
"Filename %s is a file, cannot replace it" % filename)
return
else:
client.alert(_(u'Folder name %s already exists. '
'Please choose another one or delete existing one then try again.') % filename)
return
# Now do the export
singlePageExport = SinglePageExport(stylesDir, filename, \
imagesDir, scriptsDir, templatesDir)
singlePageExport.export(self.package, printFlag)
except Exception, e:
client.alert(_('SAVE FAILED!\n%s' % str(e)))
raise
# Show the newly exported web site in a new window
if not printFlag:
self._startFile(filename)
# and return a string of the actual directory name,
# in case the package name was added, etc.:
return filename.abspath().encode('utf-8')
# WARNING: the above only returns the RELATIVE pathname
def exportWebSite(self, client, filename, stylesDir):
"""
Export 'client' to a web site,
'webDir' is just read from config.webDir
'stylesDir' is where to copy the style sheet information from
"""
try:
# filename is a directory where we will export the website to
# We assume that the user knows what they are doing
# and don't check if the directory is already full or not
# and we just overwrite what's already there
filename = Path(filename)
# Append the package name to the folder path if necessary
if filename.basename() != self.package.name:
filename /= self.package.name
if not filename.exists():
filename.makedirs()
elif not filename.isdir():
client.alert(_(u'Filename %s is a file, cannot replace it') %
filename)
log.error("Couldn't export web page: "+
"Filename %s is a file, cannot replace it" % filename)
return
else:
client.alert(_(u'Folder name %s already exists. '
'Please choose another one or delete existing one then try again.') % filename)
return
# Now do the export
websiteExport = WebsiteExport(self.config, stylesDir, filename)
websiteExport.export(self.package)
except Exception, e:
client.alert(_('EXPORT FAILED!\n%s') % str(e))
raise
# Show the newly exported web site in a new window
self._startFile(filename)
def exportWebZip(self, client, filename, stylesDir):
try:
log.debug(u"exportWebsite, filename=%s" % filename)
filename = Path(filename)
# Do the export
filename = self.b4save(client, filename, '.zip', _(u'EXPORT FAILED!'))
websiteExport = WebsiteExport(self.config, stylesDir, filename)
websiteExport.exportZip(self.package)
except Exception, e:
client.alert(_('EXPORT FAILED!\n%s' % str(e)))
raise
client.alert(_(u'Exported to %s') % filename)
def exportText(self, client, filename):
try:
filename = Path(filename)
log.debug(u"exportWebsite, filename=%s" % filename)
# Append an extension if required
if not filename.lower().endswith('.txt'):
filename += '.txt'
if Path(filename).exists():
msg = _(u'"%s" already exists.\nPlease try again with a different filename') % filename
client.alert(_(u'EXPORT FAILED!\n%s' % msg))
return
# Do the export
textExport = TextExport(filename)
textExport.export(self.package)
except Exception, e:
client.alert(_('EXPORT FAILED!\n%s' % str(e)))
raise
client.alert(_(u'Exported to %s') % filename)
def exportIpod(self, client, filename):
"""
Export 'client' to an iPod Notes folder tree
'webDir' is just read from config.webDir
"""
try:
# filename is a directory where we will export the notes to
# We assume that the user knows what they are doing
# and don't check if the directory is already full or not
# and we just overwrite what's already there
filename = Path(filename)
# Append the package name to the folder path if necessary
if filename.basename() != self.package.name:
filename /= self.package.name
if not filename.exists():
filename.makedirs()
elif not filename.isdir():
client.alert(_(u'Filename %s is a file, cannot replace it') %
filename)
log.error("Couldn't export web page: "+
"Filename %s is a file, cannot replace it" % filename)
return
else:
client.alert(_(u'Folder name %s already exists. '
'Please choose another one or delete existing one then try again.') % filename)
return
# Now do the export
ipodExport = IpodExport(self.config, filename)
ipodExport.export(self.package)
except Exception, e:
client.alert(_('EXPORT FAILED!\n%s') % str(e))
raise
client.alert(_(u'Exported to %s') % filename)
def exportScorm(self, client, filename, stylesDir, scormType):
"""
Exports this package to a scorm package file
"""
try:
filename = Path(filename)
log.debug(u"exportScorm, filename=%s" % filename)
# Append an extension if required
if not filename.lower().endswith('.zip'):
filename += '.zip'
if Path(filename).exists():
msg = _(u'"%s" already exists.\nPlease try again with a different filename') % filename
client.alert(_(u'EXPORT FAILED!\n%s' % msg))
return
# Do the export
scormExport = ScormExport(self.config, stylesDir, filename, scormType)
scormExport.export(self.package)
except Exception, e:
client.alert(_('EXPORT FAILED!\n%s' % str(e)))
raise
client.alert(_(u'Exported to %s') % filename)
def exportIMS(self, client, filename, stylesDir):
"""
Exports this package to a ims package file
"""
try:
log.debug(u"exportIMS")
# Append an extension if required
if not filename.lower().endswith('.zip'):
filename += '.zip'
if Path(filename).exists():
msg = _(u'"%s" already exists.\nPlease try again with a different filename') % filename
client.alert(_(u'EXPORT FAILED!\n%s' % msg))
return
# Do the export
imsExport = IMSExport(self.config, stylesDir, filename)
imsExport.export(self.package)
except Exception, e:
client.alert(_('EXPORT FAILED!\n%s' % str(e)))
raise
client.alert(_(u'Exported to %s' % filename))
# Utility methods
def _startFile(self, filename):
"""
Launches an exported web site or page
"""
if hasattr(os, 'startfile'):
try:
os.startfile(filename)
except UnicodeEncodeError:
os.startfile(filename.encode(Path.fileSystemEncoding))
elif sys.platform[:6] == "darwin":
import webbrowser
filename /= 'index.html'
webbrowser.open('file://'+filename)
else:
filename /= 'index.html'
log.debug(u"firefox file://"+filename+"&")
os.system("firefox file://"+filename+"&")
def _loadPackage(self, client, filename, newLoad=True,
destinationPackage=None):
"""Load the package named 'filename'"""
try:
encoding = sys.getfilesystemencoding()
if encoding is None:
encoding = 'utf-8'
filename2 = toUnicode(filename, encoding)
log.debug("filename and path" + filename2)
# see if the file exists AND is readable by the user
try:
open(filename2, 'rb').close()
except IOError:
filename2 = toUnicode(filename, 'utf-8')
try:
open(filename2, 'rb').close()
except IOError:
client.alert(_(u'File %s does not exist or is not readable.') % filename2)
return None
package = Package.load(filename2, newLoad, destinationPackage)
if package is None:
raise Exception(_("Couldn't load file, please email file to bugs@exelearning.org"))
except Exception, exc:
if log.getEffectiveLevel() == logging.DEBUG:
client.alert(_(u'Sorry, wrong file format:\n%s') % unicode(exc))
else:
client.alert(_(u'Sorry, wrong file format'))
log.error(u'Error loading package "%s": %s' % (filename2, unicode(exc)))
log.error(u'Traceback:\n%s' % traceback.format_exc())
raise
return package