First database steps in kisspidb.
authordanigm <dani@danigm.net>
Sun, 19 Apr 2009 20:32:51 +0000 (22:32 +0200)
committerdanigm <dani@danigm.net>
Sun, 19 Apr 2009 20:32:51 +0000 (22:32 +0200)
Module login started but not working yet.
Better default theme.

17 files changed:
index.py
kisspi.py
kisspidb.py [new file with mode: 0644]
login.py [deleted file]
modules/hello/hello.py
modules/login/__init__.py [new file with mode: 0644]
modules/login/database.py [new file with mode: 0644]
modules/login/login.py [new file with mode: 0644]
modules/login/templates/login.html [new file with mode: 0644]
modules/login/templates/register.html [new file with mode: 0644]
modules/modconf
modules/testmod/testmod.py
static/themes/default/css/style.css
static/themes/default/images/alert.png
static/themes/default/images/error.png
static/themes/default/images/kisspi.svg
static/themes/default/templates/master.html

index c5bdbb2..e1b4dde 100644 (file)
--- a/index.py
+++ b/index.py
@@ -60,7 +60,6 @@ class index:
                 if not f:
                     raise web.notfound()
 
-                f.kisspi = kisspi
                 function = getattr(f, method)
                 if fargs:
                     returned = function(*fargs)
index 542dc3c..7a5a332 100644 (file)
--- a/kisspi.py
+++ b/kisspi.py
@@ -10,6 +10,7 @@ import sys
 import re
 import web
 import utils
+import kisspidb as db
 
 MODULES = {}
 THEME = 'default'
@@ -51,13 +52,13 @@ def template(title='', body='', path='', method='GET'):
     head, pre_body, post_body, left, foot = 'head', 'pre-body', 'post-body', 'left', 'foot'
     default_modules = modconf()
 
-    head = parse_layaout(default_modules['head'], path, method)
-    pre_body = parse_layaout(default_modules['pre_body'], path, method)
-    post_body = parse_layaout(default_modules['post_body'], path, method)
-    left = parse_layaout(default_modules['left'], path, method)
-    foot = parse_layaout(default_modules['foot'], path, method)
+    head = parse_layaout(default_modules['head'], path)
+    pre_body = parse_layaout(default_modules['pre_body'], path)
+    post_body = parse_layaout(default_modules['post_body'], path)
+    left = parse_layaout(default_modules['left'], path)
+    foot = parse_layaout(default_modules['foot'], path)
     if not body:
-        body = parse_layaout(default_modules['default'], path, method)
+        body = parse_layaout(default_modules['default'], path)
         title = DEFAULT_TITLE
 
     render = get_render()
@@ -71,7 +72,7 @@ def template(title='', body='', path='', method='GET'):
     return render.master(title, head, pre_body, body, post_body, left,
             foot, errors=e, msgs=m)
 
-def parse_layaout(args, real_path, method):
+def parse_layaout(args, real_path):
     '''
     template auxiliar function.
 
@@ -80,8 +81,6 @@ def parse_layaout(args, real_path, method):
 
     real_path is the url path that it's visited now.
 
-    method is GET or POST
-
     Returns the html code returned by all modules in args joined
     '''
 
@@ -96,9 +95,9 @@ def parse_layaout(args, real_path, method):
             mod = MODULES[mod_name]
             # Getting the module class to call
             f, fargs = parse_url(mod, path)
-            function = getattr(f, method)
+            function = f.GET
             # Calling the module
-            to_ret += function(*fargs)
+            to_ret += str(function(*fargs))
     return to_ret
 
 def modconf(file='modules/modconf'):
@@ -147,3 +146,7 @@ def redirect(path):
 def get_render():
     return web.template.render('static/themes/'+THEME+'/templates')
 
+def get_module_render(module):
+    return web.template.render('modules/'+module+'/templates')
+
+websafe = web.net.websafe
diff --git a/kisspidb.py b/kisspidb.py
new file mode 100644 (file)
index 0000000..6fac057
--- /dev/null
@@ -0,0 +1,51 @@
+'''
+# EXAMPLE TABLE
+
+from hashlib import sha256 as sha
+from sqlalchemy import *
+
+Base = kisspi.db.base()
+
+# sqlalchemy definition
+
+class User(Base):
+    __tablename__ = 'users'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String(20), unique=True)
+    password = Column(String(60))
+
+    def set_password(self, password):
+        self.password = sha(password).hexdigest()
+
+    def __init__(self, name, password):
+        self.name = name
+        self.set_password(password)
+
+kisspi.db.create(Base)
+
+# MAKING QUERIES
+
+session = kisspi.db.connect()
+session.query(User).filter(User.name == 'danigm').first()
+'''
+
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base as base
+from sqlalchemy.orm import sessionmaker
+
+DATABASE = 'sqlite:///database.sqlite'
+
+def connect():
+    database = DATABASE
+    db = create_engine(database, echo=False)
+    Session = sessionmaker(bind=db, autoflush=True)
+    session = Session()
+    return session
+
+def create(Base):
+    database = DATABASE
+    db = create_engine(database, echo=False)
+    metadata = Base.metadata
+    metadata.create_all(db)
+
diff --git a/login.py b/login.py
deleted file mode 100644 (file)
index ba5de60..0000000
--- a/login.py
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-'''
-This controller implements:
-    /login
-    /logout
-    /register
-'''
-
-import random
-import web
-from web import form
-from utils import authenticated, templated, flash
-
-import gecoc.gecolib as gecolib
-
-session = web.ses
-
-vname = form.regexp("\w*$", 'Debe ser Alphanumerico')
-vpass = form.regexp(r".{3,20}", 'Debe estar entre 3 y 20 caracteres')
-
-form_login = form.Form(
-    form.Textbox("username", vname, description="Usuario"),
-    form.Password("password", vpass, description="Contraseña"),
-)
-
-def generate_reg_form(op1, op2):
-    form_reg = form.Form(
-        form.Textbox("rusername", vname, description="Usuario"),
-        form.Password("rpassword", vpass, description="Contraseña"),
-        form.Password("password2", description="Confirmación de contraseña"),
-        form.Textbox("captcha", description="captcha %s + %s = " % (op1, op2)),
-        validators = [
-            form.Validator("Las contraseñas no coinciden",
-                lambda i: i.rpassword == i.password2),
-            form.Validator("No sabes sumar? Usa la calculadora si eso...",
-                lambda i: int(i.captcha) == op1 + op2),
-            ])
-    return form_reg
-
-class login:
-    render = web.template.render('templates')
-
-    @templated(css='style',
-            js='jquery-1.3.1.min login',
-            title='GECO Web Client')
-    def GET(self):
-        lform = form_login()
-        op1 = random.randint(1,10) 
-        op2 = random.randint(1,10)
-        rform = generate_reg_form(op1, op2)
-        session.rform = (op1, op2)
-
-        return self.render.login(lform, rform)
-
-    @templated(css='style', 
-            js='jquery-1.3.1.min login',
-            title='GECO Web Client')
-    def POST(self):
-        lform = form_login()
-        if not lform.validates():
-            return self.render.login(form_login=lform)
-
-        values = web.input()
-        name = values['username']
-        pwd = values['password']
-
-        gso = gecolib.GSO(xmlrpc_server=web.SERVER)
-        gso.auth(name, pwd)
-        
-        if gso.name:
-            session.username = name
-            session.gso = gso.cookie
-        else:
-            flash("Usuario o contraseña incorrectos", "error")
-            raise web.seeother('/login')
-
-        raise web.seeother('/index')
-
-class logout:
-    @authenticated
-    def GET(self):
-        username = session.get('username', '')
-        session.username = ''
-        cookie = session.get('gso', '')
-        gso = gecolib.GSO(xmlrpc_server=web.SERVER, cookie=cookie)
-        gso.logout()
-        session.gso = ''
-        flash("Usuario desautenticado")
-        raise web.seeother('/index')
-
-class register:
-    render = web.template.render('templates')
-
-    @templated(css='style', title='GECO Web Client')
-    def POST(self):
-        rform = generate_reg_form(*session.rform)
-        if not rform:
-            raise web.seeother('/login')
-
-        if not rform.validates():
-            return self.render.login(form_reg=rform)
-        else:
-            gso = gecolib.GSO(xmlrpc_server=web.SERVER)
-
-            values = web.input()
-            name = values['rusername']
-            pwd = values['rpassword']
-
-            if gso.check_user_name(name):
-                errors = [u"%s no está disponible" % name]
-                flash(errors, 'error')
-                return self.render.login(form_reg=rform)
-            else:
-                gso.register(name, pwd)
-
-            flash([u"Registrado con exito %s" % name])
-            raise web.seeother("/login")
index f9f165a..2edf09d 100644 (file)
@@ -4,33 +4,32 @@ Example kisspi module controller
 Every controller is a class with GET/POST methods. That functions
 returns html that it's embebed inside the cms layaout.
 
-Inside that methods you can use self.kisspi module to:
+Inside that methods you can use kisspi module to:
     - use sessions:
-        self.kisspi.get_session()
+        kisspi.get_session()
     - get input:
-        self.kisspi.get_input()
+        kisspi.get_input()
         if you try to upload a file, use webpy syntax
-        self.kisspi.get_input(myfile={})
+        kisspi.get_input(myfile={})
     - redirect:
-        self.kisspi.redirect(new_url)
+        kisspi.redirect(new_url)
     - other modules:
-        self.kisspi.MODULES
+        kisspi.MODULES
 '''
 
+import kisspi
+
 class Hello:
-    kisspi = None
     title = 'Hello test'
     def GET(self, *args):
         return "hello everyone " + str(args)
 
 class Number:
-    kisspi = None
     title = 'Hello test'
     def GET(self, number):
         return "Hola al grupo %s" % number
 
 class Upload:
-    kisspi = None
     title = 'Hello test'
     def GET(self):
         form = '''
@@ -39,12 +38,12 @@ class Upload:
                 <input type="submit" name="upload"/>
             </form>
         '''
-        session = self.kisspi.get_session()
-        return  'uploaded: %s' % session.get('uploaded', '') + form
+        session = kisspi.get_session()
+        return  'uploaded: %s' % kisspi.websafe(session.get('uploaded', '')) + form
 
     def POST(self):
-        session = self.kisspi.get_session()
-        input = self.kisspi.get_input()
+        session = kisspi.get_session()
+        input = kisspi.get_input()
         session.uploaded = input.text
         
-        self.kisspi.redirect('/hello/up')
+        kisspi.redirect('/hello/up')
diff --git a/modules/login/__init__.py b/modules/login/__init__.py
new file mode 100644 (file)
index 0000000..76725df
--- /dev/null
@@ -0,0 +1,18 @@
+'''
+Login kisspi module
+'''
+
+import login
+
+urls = (
+        ('logout', login.Logout),
+        ('register', login.Register),
+        ('user', login.User),
+        ('left', login.Left),
+        ('', login.Login),
+        )
+
+admin = None
+# Add page
+add = None
+body = login.Login
diff --git a/modules/login/database.py b/modules/login/database.py
new file mode 100644 (file)
index 0000000..cb40e2b
--- /dev/null
@@ -0,0 +1,30 @@
+import kisspi
+
+from hashlib import sha256 as sha
+from sqlalchemy import *
+
+Base = kisspi.db.base()
+
+# sqlalchemy definition
+
+class User(Base):
+    __tablename__ = 'users'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String(20), unique=True)
+    password = Column(String(60))
+
+    def set_password(self, password):
+        self.password = sha(password).hexdigest()
+
+    def __init__(self, name, password):
+        self.name = name
+        self.set_password(password)
+
+#kisspi.db.create(Base)
+#
+## MAKING QUERIES
+#
+#session = kisspi.db.connect()
+#session.query(User).filter(User.name == 'danigm').first()
+
diff --git a/modules/login/login.py b/modules/login/login.py
new file mode 100644 (file)
index 0000000..5d27109
--- /dev/null
@@ -0,0 +1,114 @@
+'''
+Login, user and role module for kisspi. That module provides a simple
+authentication and account management.
+'''
+
+import kisspi
+import database
+import random
+
+session = kisspi.get_session()
+render = kisspi.get_module_render('login')
+
+form = kisspi.web.form
+vname = form.regexp("\w*$", 'Alphanumeric')
+vpass = form.regexp(r".{3,20}", 'Between 3 and 20 chars')
+
+form_login = form.Form(
+        form.Textbox("username", vname, description="Username: "),
+        form.Password("password", vpass, description="Password: "),
+)
+
+def generate_reg_form(op1, op2):
+    form_reg = form.Form(
+        form.Textbox("username", vname, description="Username: "),
+        form.Password("password", vpass, description="Password: "),
+        form.Password("password2", description="Password confirmation: "),
+        form.Textbox("captcha", description="captcha %s + %s = " % (op1, op2)),
+        validators = [
+            form.Validator("Passwords are differents",
+                lambda i: i.password == i.password2),
+            form.Validator("You can't do a simple add. Use a calculator if you need it",
+                lambda i: int(i.captcha) == op1 + op2),
+            ])
+    return form_reg
+
+def logout():
+    del session['name']
+
+def auth():
+    if session.get('name', ''):
+        return True
+    else:
+        return False
+
+def authenticated(function):
+    def new_function(*args, **kwargs):
+        if auth():
+            return function(*args, **kwargs)
+        else:
+            kisspi.redirect('/login')
+    return new_function
+
+class Login:
+    title = 'Login'
+    def GET(self):
+        if auth():
+            kisspi.redirect('/login/user')
+
+        return render.login(form_login())
+
+    def POST(self):
+        if not form_login.validates():
+            return render.login(form_login)
+        
+        input = kisspi.get_input()
+        if input.password == 'qwerty':
+            session.name = input.username
+        else:
+            kisspi.utils.flash('Incorrect username or password',
+            'error')
+        kisspi.redirect('/login')
+
+class Logout:
+    title = 'Logout'
+    @authenticated
+    def GET(self):
+        logout()
+        kisspi.redirect('/login')
+
+class User:
+    title = 'User'
+    @authenticated
+    def GET(self):
+        return 'Hello ' + session.name
+
+class Register:
+    title = 'Register'
+    def GET(self):
+        if auth():
+            kisspi.redirect('/login/user')
+
+        op1 = random.randint(1,10) 
+        op2 = random.randint(1,10)
+        rform = generate_reg_form(op1, op2)
+        session.rform = (op1, op2)
+
+        return render.register(rform)
+    
+    def POST(self):
+        rform = generate_reg_form(*session.rform)
+        if not rform.validates():
+            return render.register(rform)
+
+        input = kisspi.get_input()
+        kisspi.utils.flash('User %s registered' % input.username)
+        kisspi.redirect('/login')
+
+class Left:
+    def GET(self):
+        if auth():
+            return '<a href="/login/user">' + session.name + '</a> <a href="/login/logout">logout</a>'
+        else:
+            return '<a href="/login">login</a> / <a href="/login/register">register</a>'
+
diff --git a/modules/login/templates/login.html b/modules/login/templates/login.html
new file mode 100644 (file)
index 0000000..fab40e8
--- /dev/null
@@ -0,0 +1,12 @@
+$def with (form_login=None)
+
+$if form_login:
+    <div id="login">
+        <fieldset>
+            <legend>Login</legend>
+            <form action="/login" method="POST">
+                $:form_login.render()
+                <button type="submit" id="acc">Login</button>
+            </form>
+        </fieldset>
+    </div>
diff --git a/modules/login/templates/register.html b/modules/login/templates/register.html
new file mode 100644 (file)
index 0000000..c7c6e66
--- /dev/null
@@ -0,0 +1,13 @@
+$def with (form_reg=None)
+
+$if form_reg:
+    <div id="register">
+        <fieldset>
+            <legend>Register</legend>
+            <p> If you don't have an account yet, create one</p>
+            <form action="/login/register" method="POST">
+                $:form_reg.render()
+                <button type="submit" id="reg">Register</button>
+            </form>
+        </fieldset>
+    </div>
index c165bec..1314848 100644 (file)
@@ -3,5 +3,6 @@
 head testmod/head .*
 pre_body testmod/prebody .*
 post_body testmod/postbody .*
+left login/left .*
 left testmod/left .*
 default testmod/DEFAULT_BODY .*
index 648c099..bdaa853 100644 (file)
@@ -1,5 +1,4 @@
 class Test:
-    kisspi = None
     title = 'testmod'
     def GET(self, arg):
         return '<div style="border: 1px dashed gray; background-color: #ffe371; padding: 3px;">%s</div>' % arg
index 4d97104..f64b7eb 100644 (file)
@@ -8,9 +8,12 @@
  *
  */
 
+th {
+    text-align: left;
+}
+
 body{
-    background: #65c5c5 url('../images/earth.png') no-repeat scroll 90% 90%;
-    min-height: 800px;
+    background: #65c5c5 url('../images/earth.png') no-repeat scroll 90% 99%;
     margin: 0;
 }
 
@@ -19,10 +22,13 @@ img {
 }
 
 
-input, textarea, button {
-    border: 2px solid gray;
-    border-top: 1px solid green;
-    border-left: 1px solid green;
+input, textarea {
+    border: 0px;
+}
+fieldset{
+    border: 2px solid green;
+    color: green;
+    font-weight: bold;
 }
 
 textarea{ width: 100%; }
@@ -83,6 +89,7 @@ div {
 
 #all-left {
     width: 180px;
+    clear:both;
     float: left;
     min-height: 100px;
 }
@@ -107,10 +114,30 @@ div {
 
 /* FOOT */ 
 
-#foot{
+#nofloat{
     clear: both;
-    margin-top: 200px;
+}
+#foot{
     margin: 2em;
+    margin-top: 340px;
     color: #ececec;
 }
 
+/* ERROR and ALERT */
+
+#error{
+    padding-left: 40px;
+    border: 2px dashed #5e0000;
+    background: transparent url('../images/error.png') no-repeat scroll 10px 50%;
+    font-weight: bold;
+    color: #5e0000;
+    min-height: 40px;
+}
+#messages {
+    padding-left: 40px;
+    border: 2px dashed green;
+    background: transparent url('../images/alert.png') no-repeat scroll 10px 50%;
+    color: green;
+    font-weight: bold;
+    min-height: 40px;
+}
index 820006f..3affd01 100644 (file)
Binary files a/static/themes/default/images/alert.png and b/static/themes/default/images/alert.png differ
index 9690de5..02b7f03 100644 (file)
Binary files a/static/themes/default/images/error.png and b/static/themes/default/images/error.png differ
index af1c17f..63362b4 100644 (file)
   <defs
      id="defs4">
     <linearGradient
+       id="linearGradient3486"
+       inkscape:collect="always">
+      <stop
+         id="stop3488"
+         offset="0"
+         style="stop-color:#72a60c;stop-opacity:1" />
+      <stop
+         id="stop3490"
+         offset="1"
+         style="stop-color:#cdf57e;stop-opacity:1" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3470">
+      <stop
+         style="stop-color:#ff8383;stop-opacity:1;"
+         offset="0"
+         id="stop3472" />
+      <stop
+         style="stop-color:#ff4848;stop-opacity:1"
+         offset="1"
+         id="stop3474" />
+    </linearGradient>
+    <linearGradient
        id="linearGradient14196">
       <stop
          style="stop-color:#cdf57e;stop-opacity:1"
        fx="62.225393"
        fy="-3.4420195"
        r="10.081216" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3470"
+       id="radialGradient3476"
+       cx="312.53717"
+       cy="505.0134"
+       fx="312.53717"
+       fy="505.0134"
+       r="35.785614"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-0.9573692,-1.0640174,1.1453075,-1.0305143,-35.386842,1323.1129)" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3486"
+       id="radialGradient3484"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-0.9573692,-1.0640174,1.1453075,-1.0305143,-35.386842,1323.1129)"
+       cx="312.53717"
+       cy="505.0134"
+       fx="312.53717"
+       fy="505.0134"
+       r="35.785614" />
   </defs>
   <sodipodi:namedview
      id="base"
            style="opacity:1;fill:#ffffff;fill-opacity:0.66666667000000002;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.37810944000000002" />
       </g>
     </g>
+    <path
+       transform="translate(240,-80)"
+       sodipodi:type="arc"
+       style="fill:url(#radialGradient3476);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="path3468"
+       sodipodi:cx="254.47549"
+       sodipodi:cy="483.76852"
+       sodipodi:rx="35.785614"
+       sodipodi:ry="35.785614"
+       d="M 290.26111,483.76852 A 35.785614,35.785614 0 1 1 218.68988,483.76852 A 35.785614,35.785614 0 1 1 290.26111,483.76852 z"
+       inkscape:export-filename="/home/danigm/Projects/kisspi/static/themes/default/images/error.png"
+       inkscape:export-xdpi="40.239635"
+       inkscape:export-ydpi="40.239635" />
+    <text
+       xml:space="preserve"
+       style="font-size:66.67565918px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:100%;writing-mode:lr-tb;text-anchor:middle;opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.37810944;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Semi-Bold"
+       x="494.2691"
+       y="428.3421"
+       id="text2694"
+       sodipodi:linespacing="100%"><tspan
+         sodipodi:role="line"
+         id="tspan2696"
+         x="494.2691"
+         y="428.3421">X</tspan></text>
+    <path
+       inkscape:export-ydpi="40.239635"
+       inkscape:export-xdpi="40.239635"
+       inkscape:export-filename="/home/danigm/Projects/kisspi/static/themes/default/images/alert.png"
+       d="M 290.26111,483.76852 A 35.785614,35.785614 0 1 1 218.68988,483.76852 A 35.785614,35.785614 0 1 1 290.26111,483.76852 z"
+       sodipodi:ry="35.785614"
+       sodipodi:rx="35.785614"
+       sodipodi:cy="483.76852"
+       sodipodi:cx="254.47549"
+       id="path3478"
+       style="fill:url(#radialGradient3484);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       sodipodi:type="arc"
+       transform="translate(324,-80)" />
+    <text
+       sodipodi:linespacing="100%"
+       id="text3480"
+       y="428.3421"
+       x="578.2691"
+       style="font-size:66.67565918px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:100%;writing-mode:lr-tb;text-anchor:middle;opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.37810944;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Semi-Bold"
+       xml:space="preserve"><tspan
+         y="428.3421"
+         x="578.2691"
+         id="tspan3482"
+         sodipodi:role="line">!</tspan></text>
   </g>
 </svg>
index 5559c63..7dfcff5 100644 (file)
@@ -26,9 +26,6 @@ $code:
         <div id="container">
             $if msgs:
                 <div id="messages">
-                    <div class="floating">
-                        <img src="/static/themes/$theme/images/alert.png"/>
-                    </div>
                     <ul>
                         $for msg in msgs:
                         <li>$:msg</li>
@@ -36,9 +33,6 @@ $code:
                 </div>
             $if errors:
                 <div id="error">
-                    <div class="floating">
-                        <img src="/static/themes/$theme/images/error.png"/>
-                    </div>
                     <ul>
                         $for error in errors:
                         <li>$:error</li>
@@ -62,6 +56,8 @@ $code:
             </div>
         </div>
 
+        <div id="nofloat"></div>
+
         <div id="foot">
             $:foot
             This website is powered by <a href="http://trac.danigm.net/kisspi">kisspi</a><br/>