Divmod : Mantissa : Blog Tutorial

Read the MantissaWikiTutorial first, if didn't do so already.

I didn't have the time to follow my instructions and check this tutorial for completeness, yet. If you get stuck, feel free to contact me. The steps are too big and need more explanations. If you have the time to fix this, please go ahead.

-- Karl Bartel <karlb@gmx.net>

Things to learn in this tutorial:

  • Using private pages
  • Using Athena to comunicate between browser and server
  • Modifying HTML code with Javascript

Part 1: Creating an Offering and Running it in a Mantissa Server

This is much like in the MantissaWikiTutorial, but this time we will use both a public and a private page.

Create Skeleton

axiomatic project -n blog
cd Blog
axiomatic -d blogtut.axiom mantissa
axiomatic -d blogtut.axiom web --static static/blog:blog/static/

Note: windows users,  static/blog:blog/static/ is a path, you must use the windows version of the path  static/blog;blog/static

The private page already exists in the skeletion, so let's add a public page.

Add Public Page

Add blog_model.BlogPublicPage to xmantissa/plugins/blogoff.py

appPowerups = (
    blog_model.BlogStart,
    blog_model.BlogPublicPage,
)

Create the file 'blog/themes/base/blog-public.html' with the following content:

<div xmlns="http://www.w3.org/1999/xhtml"
     xmlns:nevow="http://nevow.com/ns/nevow/0.1">
        <h1>Blog Test</h1>
</div>

Create the view for the public page in blog/blog_view.py

from nevow import loaders, inevow
from nevow.rend import Page

class BlogPublicView(Page):

    docFactory = loaders.xmlfile('blog/themes/base/blog-public.html')

    def __init__(self, original):
        Page.__init__(self)
        self.original = original

    def customizeFor(self, avatar):
        return self

... and the model in blog/blog_model.py .

class BlogPublicPage(Item, InstallableMixin):
    # This object can be browsed from the web
    # From the mantissa perspective this is the model in the MVC pattern
    implements(ixmantissa.IPublicPage)

    schemaVersion = 1                  # First version of this object.
    typeName = 'blog_public_page'      # Database table name.

    name = text()               # We must have at least one attribute - model
                                # objects must store data.
    installedOn = reference()

    def installOn(self, other):
        super(BlogPublicPage, self).installOn(other)
        other.powerUp(self, ixmantissa.IPublicPage)

    def getResource(self):
        from blog.blog_view import BlogPublicView
        return BlogPublicView(self)

Install Offering

The same way as in MantissaWikiTutorial

Allow admin to view BlogStart

Log in as admin and use the integrated python shell (REPL) to give the admin access to his private blog page.

from blog.blog_model import BlogStart
BlogStart(store=s).installOn(s)

Part 2: Editing Blog Posts

Create Model for Blog Posts

Add the following code to blog/blog_model.py

class BlogPost(Item):
    schemaVersion = 1           # First version of this object.
    typeName = 'blog_post'      # Database table name.

    title = text()
    body = text()

Add some Fancy JavaScript Code

Add this to your blog/static/blog.js file

var posts = {};
var id_of_edited_post = null;

function change_post(id, title, body) {
    posts[id] = {
        'id': id,
        'title': title,
        'body': body
    };
    show_post(id);
}

function edit_post(id) {
    var edit_area = document.getElementById('edit_area');    
    var title_area = document.getElementById('title_area');
    var post = posts[id];
    title_area.value = post.title;
    edit_area.value = post.body;
    id_of_edited_post = id;
}

function save_post() {
    var edit_area = document.getElementById('edit_area');
    var title_area = document.getElementById('title_area');
    server.handle("save_post", id_of_edited_post, title_area.value, edit_area.value);
}

function delete_post(id) {
    delete posts[id];
    server.handle("delete_post", id);
    var row = document.getElementById(id);
    row.parentNode.removeChild(row);
}

function new_post(id) {
    var edit_area = document.getElementById('edit_area');    
    var title_area = document.getElementById('title_area');    
    title_area.value = '';
    edit_area.value = '';
    id_of_edited_post = null;
}

function init() {
    alert('a')
}

function show_post(id) {
    var post = posts[id];
/*    var new_row = '<tr id="' + post.id + '"><td>' + post.title + '</td></tr>';*/
    var row = document.getElementById(id);
    if (!row) {
        row = document.createElement('tr');
        row.setAttribute('id', post.id );
        var post_table = document.getElementById('post_table');
        post_table.insertBefore(row, post_table.firstChild);
    }
    row.innerHTML = '<td><a href="#" onclick="edit_post(' + post.id + '); return false;">' + post.title + '</td>';
    row.innerHTML += '<td style="text-align:center"><a href="#" onclick="delete_post(' + post.id + '); return false;">x</td></td>';
}

Configure Static Resources

In the shell, type:

axiomatic -d blogtut.axiom web --static static/blog:ABSOLUTE_PATH_TO_YOUR_DIR/Blog/blog/static

This will show the files in the blog/static dir when your browser accesses http://localhost:8080/static/blog/ .

Enhance View with Editing Methods

These new methods will be called from JavaScript via Nevow's great Athena.

class BlogView(Fragment):
    # This is a Fragment of a Page
    implements(ixmantissa.INavigableFragment)

    # This View will use the blog-start.html template
    fragmentName = 'blog-start'

    # We won't use LivePage or Athena
    live = True

    def goingLive(self, ctx, client):
        self.client = client

        from blog.blog_model import BlogPost
        posts = self.original.store.query(BlogPost)
        for post in posts:
            client.call('change_post', post.storeID, post.title or 'No Title', post.body)
            
    # you can only call functions starting with 'handle_' from JavaScript
    def handle_save_post(self, context, id, title, body):
        from blog.blog_model import BlogPost
        if id == 'null':
            post = BlogPost(store=self.original.store)
        else:
            post = self.original.store.getItemByID(long(id))
        post.title = unicode(title)
        post.body = unicode(body)
        self.client.call('change_post', id, title or 'No Title', body)

    def handle_delete_post(self, context, id):
        from blog.blog_model import BlogPost
        post = self.original.store.getItemByID(long(id))
        post.deleteFromStore()

    def head(self):
        # Add tags in the page <head>
        pass

Adapt Template

blog/themes/base/blog-start.html should look like this:

<div xmlns="http://www.w3.org/1999/xhtml"
     xmlns:nevow="http://nevow.com/ns/nevow/0.1">
    Welcome to the Blog Application
        <table id="post_table" style="border: 1px solid black">
                <tr><th>Topic</th><th>Delete Post</th></tr>
        </table>
        <form>
                <input type="text" size="60" id="title_area"/>
                <textarea cols="60" rows="30" id="edit_area"/>
        </form>
        <input type="button" value="Save Post" onclick="save_post()"/>
        <input type="button" value="New Post" onclick="new_post()"/>
</div>

Now log in as admin an navigate to the Blog application.

Part 3: Showing your posts to the public

Add render Method to View

class BlogPublicView(Page):

    docFactory = loaders.xmlfile('blog/themes/base/blog-public.html')

    def __init__(self, original):
        Page.__init__(self)
        self.original = original

    def customizeFor(self, avatar):
        return self

    def render_posts(self, context, data):
        # Get admin@localhost's store, where his blog posts are
        # There's certainly a better way for this!
        from axiom.store import Store
        store = Store('blogtut.axiom/files/account/localhost/admin.axiom')
            
        # format blog posts
        from blog.blog_model import BlogPost
        posts = store.query(BlogPost)
        from nevow import tags
        post_html = [
            [
                tags.Tag('dl')[
                    tags.dt(style='font-weight:bold')[post.title],
                    tags.dd[post.body],
                ]
            ]
            for post in posts
        ]
        return post_html

Adapt Template

Change blog-public.html to:

<div xmlns="http://www.w3.org/1999/xhtml"
     xmlns:nevow="http://nevow.com/ns/nevow/0.1">
        <h1>admin@localhost's blog</h1>
        <nevow:invisible nevow:render="posts" />        
</div>
jethro@divmod.org