wiki:FileUploadTutorial
Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Version 3 (modified by luca, 9 years ago) (diff)

Simplify total_data reading.

File Upload Tutorial

TurboGears 0.8a3

This is a follow up to the existing 20 minute wiki:  http://turbogears.org/docs/wiki20/index.html. At this point, it is assumed that you have:

  • installed TG
  • completed the previous tutorial
  • are ready to add to that tutorial

Here is what this tutorial is all about: File Uploads. After completing the above tutorial, I thought, 'gee, it'd be cool if I could upload files to the wiki'. And the way things go, one thing leads to another, and I wanted to download them, too. This introduces some issues, like:

  • ui for displaying the files (we'll use templates for all the common stuff)
  • ui for uploading/downloading the files
  • state issues : we don't want to upload a file to a page that doesn't exist yet

Rather than stepwise you thru my thought process and re-build this app like a real tutorial, I'll present the major issues along with the major chunks of code to address them.

BTW - you don't have to copy and paste from this page, there are zip files attached...just grab the real thing, amigo.

-Todd

Update the DB Tables

We need to update the database to add a new table, the uploaded files table. This is a many to many relationship in my example. We'll have to drop our old tables and re-create these for the relationships to match. Here we go:

  1. stop the server
  2. drop the existing tables
    • tg-admin sql drop page
    • tg-admin sql drop uploadedfile
  3. your new model.py
    from sqlobject import *
    from turbogears.database import PackageHub
    
    hub = PackageHub("toddswiki")
    __connection__ = hub
    
    # class YourDataClass(SQLObject):
    #     pass
    
    class Page(SQLObject):
            pagename = StringCol(alternateID=True, length=30)
            data = StringCol()
            attached_files = RelatedJoin('UploadedFile')
    
    class UploadedFile(SQLObject):
            filename = StringCol(alternateID=True)
            abspath = StringCol()
            size = IntCol()
            referenced_in_pages = RelatedJoin('Page')
    
  4. create the new tables
    • tg-admin sql create

Add the 'upload' Method

Here is the meat and potatoes. We add the upload method and a bunch of state logic. The idea is simple: we want to upload files and have them relate to a page. If a user uploads a previously uploaded file, we skip the upload, but we add the relationship to the db. So, multiple pages can reference a single uploaded file. And a single page can reference many files. Let's store these files in an app configured location:

  1. Add upload directory to the cfg file #def.cfg
    [global]
    
    # Upload dir
    wiki.uploads="./uploads"
    
  2. Add upload dir check to controller, create it if nec. #controller.py
    #default upload dir to ./uploads
    UPLOAD_DIR = cherrypy.config.get("wiki.uploads", os.path.join(os.getcwd(),"uploads"))
    if not os.path.exists(UPLOAD_DIR):
        os.makedirs(UPLOAD_DIR) 
    
  3. Add the 'upload' method (remember meat and potatoes) #controller.py
    	@turbogears.expose()
    	def upload(self, upload_file, pagename, new, **keywords):
    		try:
    			p = Page.byPagename(pagename)
    		except SQLObjectNotFound:
    			turbogears.flash("Must save page first")
    			raise cherrypy.HTTPRedirect(turbogears.url("/%s" % pagename))
    		
    		data = upload_file.file.read()
    		target_file_name = os.path.join(os.getcwd(),UPLOAD_DIR,upload_file.filename)
    		try:
    			u =  UploadedFile.byFilename(upload_file.filename)
    			turbogears.flash("File already uploaded: %s is already at %s" %  (upload_file.filename, target_file_name))
    		except SQLObjectNotFound:
    			f = open(target_file_name, 'w')
    			f.write(data)
    			f.close
    			turbogears.flash("File uploaded successfully: %s saved as : %s" % (upload_file.filename, target_file_name))
    			u = UploadedFile(filename=upload_file.filename, abspath=target_file_name, size=0)
    			
    		Page.byPagename(pagename).addUploadedFile(u)
    		raise cherrypy.HTTPRedirect(turbogears.url("/%s" % pagename))
    
  4. Note: the above uses the db to determine the page state. If the page doesn't exist in the db, then we cannot relate an uploaded file to it:
    		try:
    			p = Page.byPagename(pagename)
    		except SQLObjectNotFound:
    			turbogears.flash("Must save page first")
    			raise cherrypy.HTTPRedirect(turbogears.url("/%s" % pagename))
    

Add 'upload' to the page.kid

  1. Here's the form that calls the upload method (note we use a 'post'). Pretty simple. I have the hidden variable 'new' in here b/c I was going to use that for state tracking in the same way that the original tutorial did. For some reason, this wasn't working for me so I use the database instead, see above comment. #page.kid
    	<form action="upload" method="post" enctype="multipart/form-data">
    		<input type="hidden" name="pagename" py:attrs="value=pagename"/>
    		<input type="hidden" name="new" value="${new}"/>
    		filename: <input type="file" name="upload_file"/><br/>
    		<input type="submit" name="submit_upload" value="Upload"/>
    	</form>
    

Add the goo to display the file listings

  1. Here is the code:
    	<ul>
    		Attached Files:
    		<li py:for="filename in uploads"><a href="/download?filename=${filename}" py:content="filename">Filenamehere.</a></li>
    	</ul>
    
  2. Add this code to page.kid and edit.kid
  3. I've appended to this tutorial my notes for generalizing this with templates (see below)

Add the 'download' method

  1. cherrypy.lib.cptools.serveFile makes this super simple: #controller.py
    	@turbogears.expose()
    	def download(self, filename):
    		uf = UploadedFile.byFilename(filename)
    		return cherrypy.lib.cptools.serveFile(uf.abspath, "application/x-download", "attachment", uf.filename)
    

Add error handling to the index method, just in case publish parts barfs

#controller.py

	@turbogears.expose(html="toddswiki.templates.page")
	def index(self, pagename="FrontPage"):
		try:
			page = Page.byPagename(pagename)
			uploads = [item.filename for item in page.attached_files]
		except SQLObjectNotFound:
			raise cherrypy.HTTPRedirect(turbogears.url("/notfound",pagename=pagename))
		try:
			content = publish_parts(page.data, writer_name="html")["html_body"]
		except:
			content = page.data

		root = str(turbogears.url("/"))
		content = wikiwords.sub(r'<a href="%s\1">\1</a>' % root, content)
		content = content.encode("utf8")
		return dict(data=content, pagename=page.pagename, uploads=uploads)

Generalize the File listing with Kid Templates (inheritance/matching)

Make the following changes to get this working:

#master.kid

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<?python import sitetemplate ?>
<html
xmlns="http://www.w3.org/1999/xhtml"xmlns:py="http://purl.org/kid/ns#"
py:extends="sitetemplate, 'includes.kid'"

#page.kid / edit.kid

        <div py:replace="attached_files('${filename}')"/>

#includes.kid

<ul py:def="attached_files(filename)">
                Attached Files:
                <li py:for="filename in uploads"><a
href="./download?filename=${filename}"
py:content="filename">Filenamehere.</a></li>
</ul> 

For more details, see:

Attachments