Commits

Anonymous committed 9fdfb27

Version 1.0.0

Comments (0)

Files changed (274)

+Copyright (c) 2009 Public Software Group e. V., Berlin
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+include Makefile.options
+
+all::
+	make documentation
+	make accelerator
+	make libraries
+	make symlinks
+	make precompile
+
+documentation::
+	rm -f doc/autodoc.tmp
+	lua framework/bin/autodoc.lua framework/cgi-bin/ framework/env/ libraries/ > doc/autodoc.tmp
+	cat doc/autodoc-header.htmlpart doc/autodoc.tmp doc/autodoc-footer.htmlpart > doc/autodoc.html
+	rm -f doc/autodoc.tmp
+
+accelerator::
+	cd framework/accelerator; make
+
+libraries::
+	cd libraries/extos; make
+	cd libraries/mondelefant; make
+	cd libraries/multirand; make
+
+symlinks::
+	ln -s -f ../../libraries/atom/atom.lua framework/lib/
+	ln -s -f ../../libraries/extos/extos.so framework/lib/
+	ln -s -f ../../libraries/mondelefant/mondelefant.lua framework/lib/
+	ln -s -f ../../libraries/mondelefant/mondelefant_native.so framework/lib/
+	ln -s -f ../../libraries/mondelefant/mondelefant_atom_connector.lua framework/lib/
+	ln -s -f ../../libraries/multirand/multirand.so framework/lib/
+	ln -s -f ../../libraries/rocketcgi/rocketcgi.lua framework/lib/
+	ln -s -f ../../libraries/nihil/nihil.lua framework/lib/
+	ln -s -f ../../libraries/luatex/luatex.lua framework/lib/
+
+precompile::
+	rm -Rf framework.precompiled
+	rm -Rf demo-app.precompiled
+	sh framework/bin/recursive-luac framework/ framework.precompiled/
+	rm -f framework.precompiled/accelerator/Makefile
+	rm -f framework.precompiled/accelerator/webmcp_accelerator.c
+	rm -f framework.precompiled/accelerator/webmcp_accelerator.o
+	framework/bin/recursive-luac demo-app/ demo-app.precompiled/
+
+clean::
+	rm -f doc/autodoc.tmp doc/autodoc.html
+	rm -Rf framework.precompiled
+	rm -Rf demo-app.precompiled
+	rm -f demo-app/tmp/*
+	rm -f framework/lib/*
+	cd libraries/extos; make clean
+	cd libraries/mondelefant; make clean
+	cd libraries/multirand; make clean
+	cd framework/accelerator; make clean
+# C compiler command
+CC = cc
+
+# linker command
+LD = ld
+
+# filename extension for shared libraries
+SLIB_EXT = so
+
+# C compiler flags
+CFLAGS = -O2 -Wall -I /usr/include -I /usr/local/include -I /usr/local/include/lua51 -I /usr/include/lua5.1
+
+# additional C compiler flags for parts which depend on PostgreSQL
+CFLAGS_PGSQL = -I /usr/include/postgresql -I /usr/include/postgresql/server -I /usr/local/include/postgresql -I /usr/local/include/postgresql/server
+
+# linker flags
+LDFLAGS = -shared -L /usr/lib -L /usr/local/lib -L /usr/local/lib/lua51 -L /usr/lib/lua5.1
+
+# additional linker flags for parts which depend on PostgreSQL
+LDFLAGS_PGSQL =

demo-app/app/main/_filter/20_session.lua

+if cgi.cookies.session then
+  app.session = Session:by_ident(cgi.cookies.session)
+end
+if not app.session then
+  app.session = Session:new()
+  cgi.add_header('Set-Cookie: session=' .. app.session.ident .. '; path=/' )
+end
+
+request.set_csrf_secret(app.session.csrf_secret)
+
+if app.session.user then
+  locale.set{ lang = app.session.user.lang or "en" }
+end
+
+if param.get("lang") then
+  locale.set{ lang = param.get("lang") }
+end
+
+execute.inner()

demo-app/app/main/_filter/21_auth.lua

+local auth_needed = not (
+  request.get_module() == 'index'
+  and (
+    request.get_view() == 'login'
+    or request.get_action() == 'login'
+  )
+)
+
+-- if not app.session.user_id then
+--   trace.debug("DEBUG: AUTHENTICATION BYPASS ENABLED")
+--   app.session.user_id = 1
+-- end
+
+if app.session.user == nil and auth_needed then
+  trace.debug("Not authenticated yet.")
+  request.redirect{ module = 'index', view = 'login' }
+else
+  if auth_needed then
+    trace.debug("Authentication accepted.")
+  else
+    trace.debug("No authentication needed.")
+  end
+  execute.inner()
+  trace.debug("End of authentication filter.")
+end

demo-app/app/main/_filter_action/23_write_priv.lua

+if 
+  not (request.get_module() == "index" and request.get_action() == "login")
+  and not (request.get_module() == "index" and request.get_action() == "logout")
+then
+  app.session.user:require_privilege("write")
+end
+execute.inner()

demo-app/app/main/_filter_view/30_topnav.lua

+-- display navigation only, if user is logged in
+if app.session.user_id == nil then
+  execute.inner()
+  return
+end
+
+slot.select("topnav", function()
+  ui.link{
+    attr = { class = "nav" },
+    text = _"Home",
+    module = "index",
+    view = "index"
+  }
+  ui.link{
+    attr = { class = "nav" },
+    text = _"Media",
+    module = "medium"
+  }
+  ui.link{
+    attr = { class = "nav" },
+    text = _"Media types",
+    module = "media_type"
+  }
+  ui.link{
+    attr = { class = "nav" },
+    text = _"Genres",
+    module = "genre"
+  }
+  if app.session.user.admin then
+    ui.link{
+    attr = { class = "nav" },
+      text = _"Users",
+      module = "user"
+    }
+  end
+  ui.container{
+    attr = { class = "nav lang_chooser" },
+    content = function()
+      for i, lang in ipairs{"en", "de", "es"} do
+        ui.container{
+          content = function()
+            ui.link{
+              content = function()
+                ui.image{
+                  static = "lang/" .. lang .. ".png",
+                  attr = { alt = lang }
+                }
+                slot.put(lang)
+              end,
+              module = "index",
+              action = "set_lang",
+              params = { lang = lang },
+              routing = {
+                default = {
+                  mode = "redirect",
+                  module = request.get_module(),
+                  view = request.get_view(),
+                  id = param.get_id_cgi(),
+                  params = param.get_all_cgi()
+                }
+              }
+            }
+          end
+        }
+      end
+    end
+  }
+
+  ui.link{
+    attr = { class = "nav" },
+    text = _"Logout",
+    module = "index",
+    action = "logout",
+    redirect_to = {
+      ok = {
+        module = "index",
+        view = "login"
+      }
+    }
+  }
+end)
+
+execute.inner()

demo-app/app/main/_layout/default.html

+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <title>WebMCP Demo Application</title>
+    <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/style.css" />
+    <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/trace.css" />
+  </head>
+  <body>
+    <div class="layout_logo">
+      <div class="logo">
+        <img src="__BASEURL__/static/logo.png"> Demo
+      </div>
+    </div>
+    <div class="layout_topnav">
+      <div class="topnav" id="topnav">
+        <!-- WEBMCP SLOT topnav -->
+      </div>
+    </div>
+    <br style="clear: left;">
+    <div class="layout_sidenav">
+      <div class="sidenav" id="sidenav">
+        <!-- WEBMCP SLOT sidenav -->
+      </div>
+    </div>
+    <div class="layout_content">
+      <div class="layout_title">
+        <!-- WEBMCP SLOT title -->
+      </div>
+      <br style="clear: left;">
+      <div class="layout_actions">
+        <!-- WEBMCP SLOT actions -->
+      </div>
+      <div class="layout_main">
+        <!-- WEBMCP SLOT main -->
+      </div>
+      <div class="layout_trace" id="layout_trace" style="xdisplay: none">
+	    <div id="trace_show" onclick="document.getElementById('trace_content').style.display='block';this.style.display='none';">TRACE</div>
+        <div id="trace_content" style="display: none;">
+          <!-- WEBMCP SLOT trace -->
+	      <div class="trace_close" onclick="document.getElementById('trace_show').style.display='block';document.getElementById('trace_content').style.display='none';">
+	        close
+	      </div>
+	    </div>
+      </div>
+    </div>
+    <div class="layout_notice" id="layout_notice" onclick="document.getElementById('layout_notice').style.display='none';">
+      <!-- WEBMCP SLOT notice -->
+    </div>
+    <div class="layout_warning" id="layout_warning" onclick="document.getElementById('layout_warning').style.display='none';">
+      <!-- WEBMCP SLOT warning -->
+    </div>
+    <div class="layout_error" id="layout_error" onclick="document.getElementById('layout_error').style.display='none';">
+      <!-- WEBMCP SLOT error -->
+    </div>
+  </body>
+  <script>
+  </script>
+</html>

demo-app/app/main/_layout/system_error.html

+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <title>webmcp demo application</title>
+    <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/style.css" />
+    <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/trace.css" />
+  </head>
+  <body class="system_error">
+    <div class="layout_content">
+      <div class="layout_title">
+        <div class="title">
+          <br />
+          <br />
+          System message
+        </div>
+      </div>
+      <br style="clear: left;">
+      <div class="layout_actions">
+        &nbsp;
+      </div>
+      <div class="layout_main">
+        <div class="main">
+          <tt><!-- WEBMCP SLOT system_error --></tt>
+          <br />
+          <br />
+          <br />
+          <br />
+          <button onclick="window.location.reload()">Retry request</button>
+          <a href="__BASEURL__">index</a>
+        </div>
+      </div>
+    </div>
+      <div class="layout_trace" id="layout_trace" style="xdisplay: none">
+	    <div id="trace_show" onclick="document.getElementById('trace_content').style.display='block';this.style.display='none';" style="display: none;">TRACE</div>
+        <div id="trace_content">
+	        <!-- WEBMCP SLOT trace -->
+	        <div class="trace_close" onclick="document.getElementById('trace_show').style.display='block';document.getElementById('trace_content').style.display='none';">close</div>
+	    </div>
+      </div>
+  </body>
+</html>

demo-app/app/main/genre/_action/update.lua

+local genre
+local id = param.get_id()
+if id then
+  genre = Genre:by_id(id)
+else
+  genre = Genre:new()
+end
+
+if param.get("delete", atom.boolean) then
+  local name = genre.name
+  genre:destroy()
+  slot.put_into("notice", _("Genre '#{name}' deleted", {name = name}))
+  return
+end
+
+param.update(genre, "name", "description")
+
+genre:save()
+
+if id then
+  slot.put_into("notice", _("Genre '#{name}' updated", {name = genre.name}))
+else
+  slot.put_into("notice", _("Genre '#{name}' created", {name = genre.name}))
+end

demo-app/app/main/genre/index.lua

+slot.put_into("title", encode.html(_"Genres"))
+
+slot.select("actions", function()
+  if app.session.user.write_priv then
+    ui.link{
+      content = _"Create new genre",
+      module = "genre",
+      view = "show"
+    }
+  end
+end)
+
+
+local selector = Genre:new_selector():add_order_by('"name", "id"')
+
+slot.select("main", function()
+  ui.paginate{
+    selector = selector,
+    content = function()
+      ui.list{
+        records = selector:exec(),
+        columns = {
+          {
+            field_attr = { style = "float: right;" },
+            label = _"Id",
+            name = "id"
+          },
+          {
+            label = _"Name",
+            name = "name"
+          },
+          {
+            content = function(record)
+              ui.link{
+                content = _"Show",
+                module  = "genre",
+                view    = "show",
+                id      = record.id
+              }
+            end
+          },
+        }
+      }
+    end
+  }
+end)

demo-app/app/main/genre/show.lua

+local genre
+local id = param.get_id()
+if id then
+  genre = Genre:by_id(id)
+end
+
+if genre then
+  slot.put_into("title", encode.html(_"Genre"))
+else
+  slot.put_into("title", encode.html(_"New genre"))
+end
+
+slot.select("actions", function()
+  ui.link{
+    content = _"Back",
+    module = "genre"
+  }
+  if genre and app.session.user.write_priv then
+    ui.link{
+      content = _"Delete",
+      form_attr = {
+        onsubmit = "return confirm('" .. _'Are you sure?' .. "');"
+      },
+      module  = "genre",
+      action  = "update",
+      id      = genre.id,
+      params = { delete = true },
+      routing = {
+        default = {
+          mode = "redirect",
+          module = "genre",
+          view = "index"
+        }
+      }
+    }
+  end
+end)
+
+slot.select("main", function()
+  ui.form{
+    attr = { class = "vertical" },
+    record = genre,
+    readonly = not app.session.user.write_priv,
+    module = "genre",
+    action = "update",
+    id = id,
+    routing = {
+      default = {
+        mode = "redirect",
+        module = "genre",
+        view = "index"
+      }
+    },
+    content = function()
+      if id then
+        ui.field.integer{ label = _"Id", name = "id", readonly = true }
+      end
+      ui.field.text{    label = _"Name",        name = "name"                          }
+      ui.field.text{    label = _"Description", name = "description", multiline = true }
+      ui.submit{ text = _"Save" }
+    end
+  }
+end)

demo-app/app/main/index/_action/login.lua

+local user = User:by_ident_and_password(param.get('ident'), param.get('password'))
+
+if user then
+  app.session.user = user
+  app.session:save()
+  slot.put_into('notice', _'Login successful!')
+  trace.debug('User authenticated')
+else
+  slot.put_into('error', _'Invalid username or password!')
+  trace.debug('User NOT authenticated')
+  return false
+end

demo-app/app/main/index/_action/logout.lua

+if app.session then
+  app.session:destroy()
+  slot.put_into("notice", _"Logout successful")
+end

demo-app/app/main/index/_action/set_lang.lua

+app.session.user.lang = param.get("lang")
+app.session.user:save()
+
+locale.set{ lang = app.session.user.lang }
+
+slot.put_into("notice", _"Language changed")

demo-app/app/main/index/index.lua

+slot.put_into('title', encode.html(_"webmcp demo application"))
+
+slot.put_into('main', encode.html(_"Welcome to webmcp demo application"))

demo-app/app/main/index/login.lua

+slot.put_into("title", encode.html(_"Password login"))
+
+slot.select("main", function()
+
+  ui.form{
+    attr = { class = "vertical" },
+    module = "index",
+    action = "login", 
+    routing = { 
+      default = {
+        mode = "redirect",
+        module = "index",
+        view = "index"
+      }
+    },
+    content = function()
+
+      ui.container{
+        attr = { class = "lang_chooser" },
+        content = function()
+          for i, lang in ipairs{"en", "de", "es"} do
+            ui.container{
+              content = function()
+                ui.link{
+                  content = function()
+                    ui.image{
+                      static = "lang/" .. lang .. ".png",
+                      attr = { alt = lang }
+                    }
+                    slot.put(lang)
+                  end,
+                  module = "index",
+                  view = "login",
+                  params = { lang = lang }
+                }
+              end
+            }
+          end
+        end
+      }
+
+      ui.field.text{ label = _"Username", name = "ident" }
+      ui.field.text{ label = _"Password", name = "password" }
+      ui.submit{ text = _"Login" }
+    end
+  }
+end)

demo-app/app/main/media_type/_action/update.lua

+local media_type
+local id = param.get_id()
+if id then
+  media_type = MediaType:by_id(id)
+else
+  media_type = MediaType:new()
+end
+
+if param.get("delete", atom.boolean) then
+  local name = media_type.name
+  media_type:destroy()
+  slot.put_into("notice", _("Media type '#{name}' deleted", {name = name}))
+  return
+end
+
+param.update(media_type, "name", "description")
+
+media_type:save()
+
+if id then
+  slot.put_into("notice", _("Media type '#{name}' updated", {name = media_type.name}))
+else
+  slot.put_into("notice", _("Media type '#{name}' created", {name = media_type.name}))
+end

demo-app/app/main/media_type/index.lua

+slot.put_into("title", encode.html(_"Media types"))
+
+slot.select("actions", function()
+  if app.session.user.write_priv then
+    ui.link{
+      content = _"Create new media type",
+      module = "media_type",
+      view = "show"
+    }
+  end
+end)
+
+
+local selector = MediaType:new_selector():add_order_by('"name", "id"')
+
+slot.select("main", function()
+  ui.paginate{
+    selector = selector,
+    content = function()
+      ui.list{
+        records = selector:exec(),
+        columns = {
+          {
+            field_attr = { style = "float: right;" },
+            label = _"Id",
+            name = "id"
+          },
+          {
+            label = _"Name",
+            name = "name"
+          },
+          {
+            content = function(record)
+              ui.link{
+                content = _"Show",
+                module  = "media_type",
+                view    = "show",
+                id      = record.id
+              }
+            end
+          },
+        }
+      }
+    end
+  }
+end)

demo-app/app/main/media_type/show.lua

+local media_type
+local id = param.get_id()
+if id then
+  media_type = MediaType:by_id(id)
+end
+
+if media_type then
+  slot.put_into("title", encode.html(_"Media type"))
+else
+  slot.put_into("title", encode.html(_"New media type"))
+end
+
+slot.select("actions", function()
+  ui.link{
+    content = _"Back",
+    module = "media_type"
+  }
+  if media_type and app.session.user.write_priv then
+    ui.link{
+      content = _"Delete",
+      form_attr = {
+        onsubmit = "return confirm('" .. _'Are you sure?' .. "');"
+      },
+      module  = "media_type",
+      action  = "update",
+      id      = media_type.id,
+      params = { delete = true },
+      routing = {
+        default = {
+          mode = "redirect",
+          module = "media_type",
+          view = "index"
+        }
+      }
+    }
+  end
+end)
+
+slot.select("main", function()
+  ui.form{
+    attr = { class = "vertical" },
+    record = media_type,
+    readonly = not app.session.user.write_priv,
+    module = "media_type",
+    action = "update",
+    id = id,
+    routing = {
+      default = {
+        mode = "redirect",
+        module = "media_type",
+        view = "index"
+      }
+    },
+    content = function()
+      if id then
+        ui.field.integer{ label = _"Id", name = "id", readonly = true }
+      end
+      ui.field.text{    label = _"Name",        name = "name"                          }
+      ui.field.text{    label = _"Description", name = "description", multiline = true }
+      ui.submit{ text = _"Save" }
+    end
+  }
+end)

demo-app/app/main/medium/_action/update.lua

+local medium
+local id = param.get_id()
+if id then
+  medium = Medium:by_id(id)
+else
+  medium = Medium:new()
+end
+
+if param.get("delete", atom.boolean) then
+  local name = medium.name
+  medium:destroy()
+  slot.put_into("notice", _("Medium '#{name}' deleted", {name = name}))
+  return
+end
+
+param.update(medium, "media_type_id", "name", "copyprotected")
+
+medium:save()
+
+param.update_relationship{
+  param_name        = "genres",
+  id                = medium.id,
+  connecting_model  = Classification,
+  own_reference     = "medium_id",
+  foreign_reference = "genre_id"
+}
+
+for index, prefix in param.iterate("tracks") do
+  local id = param.get(prefix .. "id", atom.integer)
+  local track
+  if id then
+    track = Track:by_id(id)
+  elseif #param.get(prefix .. "name") > 0 then
+    track = Track:new()
+    track.medium_id = medium.id
+  else
+    break
+  end
+  track.position    = param.get(prefix .. "position", atom.integer)
+  track.name        = param.get(prefix .. "name")
+  track.description = param.get(prefix .. "description")
+  track.duration    = param.get(prefix .. "duration")
+  track:save()
+end
+
+
+if id then
+  slot.put_into("notice", _("Medium '#{name}' updated", {name = medium.name}))
+else
+  slot.put_into("notice", _("Medium '#{name}' created", {name = medium.name}))
+end

demo-app/app/main/medium/index.lua

+slot.put_into("title", encode.html(_"Media"))
+
+slot.select("actions", function()
+  if app.session.user.write_priv then
+    ui.link{
+      content = _"Create new medium",
+      module = "medium",
+      view = "show"
+    }
+  end
+end)
+
+
+local selector = Medium:new_selector():add_order_by('"name", "id"')
+
+slot.select("main", function()
+  ui.paginate{
+    selector = selector,
+    content = function()
+      ui.list{
+        records = selector:exec(),
+        columns = {
+          {
+            field_attr = { style = "float: right;" },
+            label = _"Id",
+            name = "id"
+          },
+          {
+            label = _"Name",
+            name = "name"
+          },
+          {
+            label = _"Copy protected",
+            name = "copyprotected"
+          },
+          {
+            content = function(record)
+              ui.link{
+                content = _"Show",
+                module  = "medium",
+                view    = "show",
+                id      = record.id
+              }
+            end
+          },
+        }
+      }
+    end
+  }
+end)

demo-app/app/main/medium/show.lua

+local medium
+local id = param.get_id()
+if id then
+  medium = Medium:by_id(id)
+end
+
+if medium then
+  slot.put_into("title", encode.html(_"Medium"))
+else
+  slot.put_into("title", encode.html(_"New medium"))
+end
+
+slot.select("actions", function()
+  ui.link{
+    content = _"Back",
+    module = "medium"
+  }
+  if medium and app.session.user.write_priv then
+    ui.link{
+      content = _"Delete",
+      form_attr = {
+        onsubmit = "return confirm(" .. encode.json(_'Are you sure?') .. ");"
+      },
+      module  = "medium",
+      action  = "update",
+      id      = medium.id,
+      params = { delete = true },
+      routing = {
+        default = {
+          mode = "redirect",
+          module = "medium",
+          view = "index"
+        }
+      }
+    }
+  end
+end)
+
+slot.select("main", function()
+  ui.form{
+    attr = { class = "vertical" },
+    record = medium,
+    readonly = not app.session.user.write_priv,
+    module = "medium",
+    action = "update",
+    id = id,
+    routing = {
+      default = {
+        mode = "redirect",
+        module = "medium",
+        view = "index"
+      }
+    },
+    content = function()
+      if id then
+        ui.field.integer{ label = _"Id", name = "id", readonly = true }
+      end
+      ui.field.select{
+        label = _"Media type",
+        name  = "media_type_id",
+        foreign_records = MediaType:new_selector():exec(),
+        foreign_id = "id",
+        foreign_name = "name"
+      }
+      ui.field.text{    label = _"Name",           name = "name"           }
+      ui.field.boolean{ label = _"Copy protected", name = "copyprotected"  }
+
+      ui.multiselect{
+        name               = "genres[]",
+        label              = _"Genres",
+        style              = "select",
+        attr = { size = 5 },
+        foreign_records    = Genre:new_selector():exec(),
+        connecting_records = medium and medium.classifications or {},
+        own_id             = "id",
+        own_reference      = "medium_id",
+        foreign_reference  = "genre_id",
+        foreign_id         = "id",
+        foreign_name       = "name",
+      }
+      local tracks = medium and medium.tracks or {}
+      for i = 1, 5 do
+        tracks[#tracks+1] = Track:new()
+      end
+      ui.list{
+        label = _"Tracks",
+        prefix = "tracks",
+        records = tracks,
+        columns = {
+          {
+            label = _"Pos",
+            name = "position",
+          },
+          {
+            label = _"Name",
+            name = "name",
+          },
+          {
+            label = _"Description",
+            name = "description",
+          },
+          {
+            label = _"Duration",
+            name = "duration",
+          },
+          {
+            content = function()
+              ui.field.hidden{ name = "id" }
+            end
+          }
+        }
+      }
+
+      ui.submit{ text = _"Save" }
+    end
+  }
+end)

demo-app/app/main/user/_action/update.lua

+local user
+local id = param.get_id()
+if id then
+  user = User:by_id(id)
+else
+  user = User:new()
+end
+
+if param.get("delete", atom.boolean) then
+  local name = user.name
+  user:destroy()
+  slot.put_into("notice", _("User '#{name}' deleted", {name = name}))
+  return
+end
+
+param.update(user, "ident", "password", "name", "write_priv", "admin")
+
+user:save()
+
+if id then
+  slot.put_into("notice", _("User '#{name}' updated", {name = user.name}))
+else
+  slot.put_into("notice", _("User '#{name}' created", {name = user.name}))
+end

demo-app/app/main/user/_filter/25_require_admin.lua

+app.session.user:require_privilege("admin")
+
+execute.inner()

demo-app/app/main/user/index.lua

+slot.put_into("title", encode.html(_"Users"))
+
+slot.select("actions", function()
+  ui.link{
+    content = _"Create new user",
+    module = "user",
+    view = "show"
+  }
+end)
+
+
+local selector = User:new_selector():add_order_by('"ident", "id"')
+
+slot.select("main", function()
+  ui.paginate{
+    selector = selector,
+    content = function()
+      ui.list{
+        records = selector:exec(),
+        columns = {
+          {
+            field_attr = { style = "float: right;" },
+            label = _"Id",
+            name = "id"
+          },
+          {
+            label = _"Ident",
+            name = "ident"
+          },
+          {
+            label = _"Name",
+            name = "name"
+          },
+          {
+            label = _"w",
+            name = "write_priv"
+          },
+          {
+            label = _"Admin",
+            name = "admin"
+          },
+          {
+            content = function(record)
+              ui.link{
+                content = _"Show",
+                module  = "user",
+                view    = "show",
+                id      = record.id
+              }
+            end
+          },
+        }
+      }
+    end
+  }
+end)

demo-app/app/main/user/show.lua

+local user
+local id = param.get_id()
+if id then
+  user = User:by_id(id)
+end
+
+if user then
+  slot.put_into("title", encode.html(_"User"))
+else
+  slot.put_into("title", encode.html(_"New user"))
+end
+
+slot.select("actions", function()
+  ui.link{
+    content = _"Back",
+    module = "user"
+  }
+  if user then
+    ui.link{
+      content = _"Delete",
+      form_attr = {
+        onsubmit = "return confirm('" .. _'Are you sure?' .. "');"
+      },
+      module  = "user",
+      action  = "update",
+      id      = user.id,
+      params = { delete = true },
+      routing = {
+        default = {
+          mode = "redirect",
+          module = "user",
+          view = "index"
+        }
+      }
+    }
+  end
+end)
+
+slot.select("main", function()
+  ui.form{
+    attr = { class = "vertical" },
+    record = user,
+    module = "user",
+    action = "update",
+    id = id,
+    routing = {
+      default = {
+        mode = "redirect",
+        module = "user",
+        view = "index"
+      }
+    },
+    content = function()
+      if id then
+        ui.field.integer{ label = _"Id", name = "id", readonly = true }
+      end
+      ui.field.text{    label = _"Ident",      name = "ident"      }
+      ui.field.text{    label = _"Password",   name = "password"   }
+      ui.field.text{    label = _"Name",       name = "name"       }
+      ui.field.boolean{ label = _"Write Priv", name = "write_priv" }
+      ui.field.boolean{ label = _"Admin",      name = "admin"      }
+      ui.submit{ text = _"Save" }
+    end
+  }
+end)

demo-app/config/demo.lua

+-- uncomment the following two lines to use C implementations of chosen
+-- functions and to disable garbage collection during the request, to
+-- increase speed:
+--
+-- require 'webmcp_accelerator'
+-- collectgarbage("stop")
+
+-- open and set default database handle
+db = assert(mondelefant.connect{
+  engine='postgresql',
+  dbname='webmcp_demo'
+})
+at_exit(function() 
+  db:close()
+end)
+function mondelefant.class_prototype:get_db_conn() return db end
+
+-- enable output of SQL commands in trace system
+function db:sql_tracer(command)
+  return function(error_info)
+    local error_info = error_info or {}
+    trace.sql{ command = command, error_position = error_info.position }
+  end
+end
+
+-- 'request.get_relative_baseurl()' should be replaced by the absolute
+-- base URL of the application, as otherwise HTTP redirects will not be
+-- standard compliant
+request.set_absolute_baseurl(request.get_relative_baseurl())
+
+-- uncomment the following lines, if you want to use a database driven
+-- tempstore (for flash messages):
+--
+-- function tempstore.save(blob)
+--   return Tempstore:create(blob)
+-- end
+-- function tempstore.pop(key)
+--   return Tempstore:data_by_key(key)
+-- end
+
+
+function mondelefant.class_prototype:by_id(id)
+  return self:new_selector()
+    :add_where{ "id = ?", id }
+    :optional_object_mode()
+    :exec()
+end

demo-app/db/schema.sql

+-- only needed for database driven tempstore (see application config)
+CREATE TABLE "tempstore" (
+        "key"           TEXT            PRIMARY KEY,
+        "data"          BYTEA           NOT NULL );
+
+-- Attention: USER is a reserved word in PostgreSQL. We use it anyway in
+-- this example. Don't forget the double quotes where neccessary.
+CREATE TABLE "user" (
+        "id"            SERIAL8         PRIMARY KEY,
+        "ident"         TEXT            NOT NULL,
+        "password"      TEXT,
+        "name"          TEXT,
+        "lang"          TEXT,
+        "write_priv"    BOOLEAN         NOT NULL DEFAULT FALSE,
+        "admin"         BOOLEAN         NOT NULL DEFAULT FALSE );
+
+CREATE TABLE "session" (
+        "ident"         TEXT            PRIMARY KEY,
+        "csrf_secret"   TEXT            NOT NULL,
+        "expiry"        TIMESTAMPTZ     NOT NULL DEFAULT NOW() + '24 hours',
+        "user_id"       INT8            REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE );
+
+CREATE TABLE "media_type" (
+        "id"            SERIAL8         PRIMARY KEY,
+        "name"          TEXT            NOT NULL,
+        "description"   TEXT );
+
+CREATE TABLE "genre" (
+        "id"            SERIAL8         PRIMARY KEY,
+        "name"          TEXT            NOT NULL,
+        "description"   TEXT );
+
+CREATE TABLE "medium" (
+        "id"            SERIAL8         PRIMARY KEY,
+        "media_type_id" INT8            NOT NULL REFERENCES "media_type" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
+        "name"          TEXT            NOT NULL,
+        "copyprotected" BOOLEAN         NOT NULL );
+
+CREATE TABLE "classification" (
+        PRIMARY KEY ("medium_id", "genre_id"),
+        "medium_id"     INT8            REFERENCES "medium" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+        "genre_id"      INT8            REFERENCES "genre" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
+
+CREATE TABLE "track" (
+        "id"            SERIAL8         PRIMARY KEY,
+        "medium_id"     INT8            NOT NULL REFERENCES "medium" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+        "position"      INT8            NOT NULL,
+        "name"          TEXT            NOT NULL,
+        "description"   TEXT,
+        "duration"      INTERVAL,
+        UNIQUE ("medium_id", "position") );
+
+INSERT INTO "user" ("ident", "password", "name", "write_priv", "admin")
+  VALUES ('admin', 'admin', 'Administrator', true, true);
+
+INSERT INTO "user" ("ident", "password", "name", "write_priv", "admin")
+  VALUES ('user', 'User', 'User', true, false);
+
+INSERT INTO "user" ("ident", "password", "name", "write_priv", "admin")
+  VALUES ('anon', 'anon', 'Anonymous', false, false);
+
+INSERT INTO "media_type" ("name", "description") VALUES ('CD', '');
+INSERT INTO "media_type" ("name", "description") VALUES ('Tape', '');
+
+INSERT INTO "genre" ("name", "description") VALUES ('Klassik', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Gospel', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Jazz', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Traditional', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Latin', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Blues', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Rhythm & blues', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Funk', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Rock', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Pop', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Country', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Electronic', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Ska / Reggea', '');
+INSERT INTO "genre" ("name", "description") VALUES ('Hip hop / Rap', '');
+

demo-app/locale/translations.de.lua

+#!/usr/bin/env lua
+return {
+["Admin"] = "Admin";
+["Are you sure?"] = "Bist Du sicher?";
+["Back"] = "Zurück";
+["Copy protected"] = "Kopiergeschützt";
+["Create new genre"] = "Neues Genre anlegen";
+["Create new media type"] = "Neuen Medientyp anlegen";
+["Create new medium"] = "Neues Medium anlegen";
+["Create new user"] = "Neuen Benutzer anlegen";
+["Delete"] = "Löschen";
+["Description"] = "Beschreibung";
+["Duration"] = "Dauer";
+["Genre"] = "Genre";
+["Genre '#{name}' created"] = "Genre '#{name}' angelegt";
+["Genre '#{name}' deleted"] = "Genre '#{name}' gelöscht";
+["Genre '#{name}' updated"] = "Genre '#{name}' aktualisiert";
+["Genres"] = "Genres";
+["Home"] = "Startseite";
+["Id"] = "Id";
+["Ident"] = "Ident";
+["Invalid username or password!"] = "Ungülter Benutzername oder Kennwort";
+["Language changed"] = "Sprache gewechselt";
+["Login"] = "Anmeldung";
+["Login successful!"] = "Anmeldung erfolgreich!";
+["Logout"] = "Abmelden";
+["Logout successful"] = "Anmeldung erfolgreich";
+["Media"] = "Medium";
+["Media type"] = "Medientyp";
+["Media type '#{name}' created"] = "Medientyp '#{name}' angelegt";
+["Media type '#{name}' deleted"] = "Medientyp '#{name}' gelöscht";
+["Media type '#{name}' updated"] = "Medientyp '#{name}' aktualisiert";
+["Media types"] = "Medientypen";
+["Medium"] = "Medium";
+["Medium '#{name}' created"] = "Medium '#{name}' angelegt";
+["Medium '#{name}' deleted"] = "Medium '#{name}' gelöscht";
+["Medium '#{name}' updated"] = "Medium '#{name}' aktualisiert";
+["Name"] = "Name";
+["New genre"] = "Neues Genre";
+["New media type"] = "Neuer Medientyp";
+["New medium"] = "Neues Medium";
+["New user"] = "Neuer Benutzer";
+["Password"] = "Kennwort";
+["Password login"] = "Anmeldung mit Kennwort";
+["Pos"] = "Pos";
+["Save"] = "Speichern";
+["Show"] = "Anzeigen";
+["Tracks"] = "Stücke";
+["User"] = "Benutzer";
+["User '#{name}' created"] = "Benutzer '#{name}' angelegt";
+["User '#{name}' deleted"] = "Benutzer '#{name}' gelöscht";
+["User '#{name}' updated"] = "Benutzer '#{name}' aktualisiert";
+["Username"] = "Benutzername";
+["Users"] = "Benutzer";
+["Welcome to webmcp demo application"] = "Willkommen zur webmcp Demo-Anwendung";
+["Write Priv"] = "Schreibrecht";
+["w"] = "w";
+["webmcp demo application"] = "webmcp Demo-Anwendung";
+}

demo-app/locale/translations.en.lua

+#!/usr/bin/env lua
+return {
+["Admin"] = false;
+["Are you sure?"] = false;
+["Back"] = false;
+["Copy protected"] = false;
+["Create new genre"] = false;
+["Create new media type"] = false;
+["Create new medium"] = false;
+["Create new user"] = false;
+["Delete"] = false;
+["Description"] = false;
+["Duration"] = false;
+["Genre"] = false;
+["Genre '#{name}' created"] = false;
+["Genre '#{name}' deleted"] = false;
+["Genre '#{name}' updated"] = false;
+["Genres"] = false;
+["Home"] = false;
+["Id"] = false;
+["Ident"] = false;
+["Invalid username or password!"] = false;
+["Language changed"] = false;
+["Login"] = false;
+["Login successful!"] = false;
+["Logout"] = false;
+["Logout successful"] = false;
+["Media"] = false;
+["Media type"] = false;
+["Media type '#{name}' created"] = false;
+["Media type '#{name}' deleted"] = false;
+["Media type '#{name}' updated"] = false;
+["Media types"] = false;
+["Medium"] = false;
+["Medium '#{name}' created"] = false;
+["Medium '#{name}' deleted"] = false;
+["Medium '#{name}' updated"] = false;
+["Name"] = false;
+["New genre"] = false;
+["New media type"] = false;
+["New medium"] = false;
+["New user"] = false;
+["Password"] = false;
+["Password login"] = false;
+["Pos"] = false;
+["Save"] = false;
+["Show"] = false;
+["Tracks"] = false;
+["User"] = false;
+["User '#{name}' created"] = false;
+["User '#{name}' deleted"] = false;
+["User '#{name}' updated"] = false;
+["Username"] = false;
+["Users"] = false;
+["Welcome to webmcp demo application"] = false;
+["Write Priv"] = false;
+["w"] = false;
+["webmcp demo application"] = false;
+}

demo-app/locale/translations.es.lua

+#!/usr/bin/env lua
+return {
+["Admin"] = "Admin";
+["Are you sure?"] = "Estás seguro?";
+["Back"] = "Atrás";
+["Copy protected"] = "Protegido anticopia";
+["Create new genre"] = "Crear un nuevo género";
+["Create new media type"] = "Crear un nuevo soporte";
+["Create new medium"] = "Crear un nuevo medio";
+["Create new user"] = "Crear un nuevo usuario";
+["Delete"] = "Borrar";
+["Description"] = "Descripción";
+["Duration"] = "Duración";
+["Genre"] = "Género";
+["Genre '#{name}' created"] = "Género '#{name}' creado";
+["Genre '#{name}' deleted"] = "Género '#{name}' borrado";
+["Genre '#{name}' updated"] = "Género '#{name}' actualizado";
+["Genres"] = "Géneros";
+["Home"] = "Inicio";
+["Id"] = "Id";
+["Ident"] = "Ident";
+["Invalid username or password!"] = "Nombre de usuario o Contraseña Incorrecta";
+["Language changed"] = "Idioma cambiado";
+["Login"] = "Login";
+["Login successful!"] = "Login realizado!";
+["Logout"] = "Logout";
+["Logout successful"] = "Logout realizado";
+["Media"] = "Medios";
+["Media type"] = "Soporte";
+["Media type '#{name}' created"] = "Soporte '#{name}' creado";
+["Media type '#{name}' deleted"] = "Soporte '#{name}' borrado";
+["Media type '#{name}' updated"] = "Soporte '#{name}' actualizado";
+["Media types"] = "Soporte";
+["Medium"] = "Medio";
+["Medium '#{name}' created"] = "Medio '#{name}' creado";
+["Medium '#{name}' deleted"] = "Medio '#{name}' borrado";
+["Medium '#{name}' updated"] = "Medio '#{name}' actualizado";
+["Name"] = "Nombre";
+["New genre"] = "Nuevo género";
+["New media type"] = "Nuevo soporte";
+["New medium"] = "Nuevo medio";
+["New user"] = "Nuevo usuario";
+["Password"] = "Contraseña";
+["Password login"] = "Login con contraseña";
+["Pos"] = "Pos";
+["Save"] = "Guardar";
+["Show"] = "Mostrar";
+["Tracks"] = "Pistas";
+["User"] = "Usuario";
+["User '#{name}' created"] = "Usuario '#{name}' creado";
+["User '#{name}' deleted"] = "Usuario '#{name}' borrado";
+["User '#{name}' updated"] = "Usuario '#{name}' actualizado";
+["Username"] = "Nombre de usuario";
+["Users"] = "Usuarios";
+["Welcome to webmcp demo application"] = "Bienvenido a la aplicación de demostración de webmcp";
+["Write Priv"] = "Permiso de escritura";
+["w"] = "w";
+["webmcp demo application"] = "Aplicación de demostración de webmcp";
+}

demo-app/model/classification.lua

+Classification = mondelefant.new_class()
+Classification.table = 'classification'
+
+Classification:add_reference{
+  mode          = 'm1',         -- many (m) Classifications can refer to one (1) Medium
+  to            = "Medium",     -- name of referenced model (quoting avoids auto-loading of model here)
+  this_key      = 'medium_id',  -- our key in the classification table
+  that_key      = 'id',         -- other key in the medium table
+  ref           = 'medium',     -- name of reference
+  back_ref      = nil,          -- not used for m1 relation!
+  default_order = nil           -- not used for m1 relation!
+}
+
+Classification:add_reference{
+  mode          = 'm1',        -- many (m) Classifications can refer to one (1) Medium
+  to            = "Genre",     -- name of referenced model (quoting avoids auto-loading of model here)
+  this_key      = 'genre_id',  -- our key in the classification table
+  that_key      = 'id',        -- other key in the genre table
+  ref           = 'genre',     -- name of reference
+  back_ref      = nil,         -- not used for m1 relation!
+  default_order = nil          -- not used for m1 relation!
+}

demo-app/model/genre.lua

+Genre = mondelefant.new_class()
+Genre.table = 'genre'
+
+Genre:add_reference{
+  mode          = '1m',               -- one (1) Genre is used for many (m) Classifications
+  to            = "Classification",   -- name of referenced model (using a string instead of reference avoids auto-loading here)
+  this_key      = 'id',               -- own key in genre table
+  that_key      = 'genre_id',         -- other key in classification table
+  ref           = 'classifications',  -- name of reference
+  back_ref      = 'genre',            -- each autoloaded Classification automatically refers back to the Genre
+  default_order = '"media_id"'        -- order Classifications by SQL expression "media_id"
+}
+
+Genre:add_reference{
+  mode                  = 'mm',              -- many (m) Genres belong to many (m) Medium entries
+  to                    = "Medium",          -- name of referenced model (quoting avoids auto-loading here)
+  this_key              = 'id',              -- (primary) key of genre table
+  that_key              = 'id',              -- (primary) key of medium talbe
+  connected_by_table    = 'classification',  -- table connecting genres with media
+  connected_by_this_key = 'genre_id',        -- key in connection table referencing genres
+  connected_by_that_key = 'medium_id',       -- key in connection table referencing media
+  ref                   = 'media',           -- name of reference
+  back_ref              = nil,               -- not used for mm relation!
+  default_order         = '"medium"."name", "medium"."id"'  -- mm references need qualified names in SQL order expression!
+}

demo-app/model/media_type.lua

+MediaType = mondelefant.new_class()
+MediaType.table = 'media_type'
+
+MediaType:add_reference{
+  mode          = '1m',             -- one (1) MediaType is set for many (m) media
+  to            = "Medium",         -- name of referenced model (quoting avoids auto-loading here)
+  this_key      = 'id',             -- own key in media_type table
+  that_key      = 'media_type_id',  -- other key in medium table
+  ref           = 'media',          -- name of reference
+  back_ref      = 'media_type',     -- each autoloaded Medium automatically refers back to the MediaType
+  default_order = '"name", "id"'    -- order media by SQL expression "name", "id"
+}

demo-app/model/medium.lua

+Medium = mondelefant.new_class()
+Medium.table = 'medium'
+
+Medium:add_reference{
+  mode          = 'm1',             -- many (m) Medium entries can refer to one (1) MediaType
+  to            = "MediaType",      -- name of referenced model (quoting avoids auto-loading here)
+  this_key      = 'media_type_id',  -- own key in medium table
+  that_key      =  'id',            -- other key in media_type table
+  ref           = 'media_type',     -- name of reference
+  back_ref      = nil,              -- not used for m1 relation!
+  default_order = nil               -- not used for m1 relation!
+}
+
+Medium:add_reference{
+  mode          = '1m',               -- one (1) Medium has many (m) Classifications
+  to            = "Classification",   -- name of referenced model (quoting avoids auto-loading here)
+  this_key      = 'id',               -- own key in medium table
+  that_key      = 'medium_id',        -- other key in classification table
+  ref           = 'classifications',  -- name of reference
+  back_ref      = 'medium',           -- each autoloaded classification automatically refers back to the Medium
+  default_order = '"genre_id"'        -- order classifications by SQL expression "genre_id"
+}
+
+Medium:add_reference{
+  mode                  = 'mm',              -- many (m) Media belong to many (m) Genres
+  to                    = "Genre",           -- name of referenced model (quoting avoids auto-loading here)
+  this_key              = 'id',              -- (primary) key of medium table
+  that_key              = 'id',              -- (primary) key of genre talbe
+  connected_by_table    = 'classification',  -- table connecting media with genres
+  connected_by_this_key = 'medium_id',       -- key in classification table referencing media
+  connected_by_that_key = 'genre_id',        -- key in classification table referencing genres
+  ref                   = 'genres',          -- name of reference
+  back_ref              = nil,               -- not used for mm relation!
+  default_order         = '"genre"."name", "genre"."id"'  -- mm references need qualified names in SQL order expression!
+}
+
+Medium:add_reference{
+  mode          = '1m',         -- one (1) Medium has many (m) Tracks
+  to            = "Track",      -- name of referenced model (quoting avoids auto-loading here)
+  this_key      = 'id',         -- own key in medium table
+  that_key      = 'medium_id',  -- other key in track table
+  ref           = 'tracks',     -- name of reference
+  back_ref      = 'medium',     -- each autoloaded classification automatically refers back to the Medium
+  default_order = '"position"'  -- order tracks by SQL expression "position"
+}

demo-app/model/session.lua

+Session = mondelefant.new_class()
+Session.table = 'session'
+Session.primary_key = { "ident" }
+
+Session:add_reference{
+  mode          = 'm1',       -- many (m) sessions refer to one (1) user
+  to            = "User",     -- name of referenced model (quoting avoids auto-loading here)
+  this_key      = 'user_id',  -- own key in session table
+  that_key      = 'id',       -- other key in user table
+  ref           = 'user',     -- name of reference
+  back_ref      = nil,        -- not used for m1 relation!
+  default_order = nil         -- not used for m1 relation!
+}
+
+local function random_string()
+  return multirand.string(
+    32,
+    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+  )
+end
+
+function Session:new()
+  local session = self.prototype.new(self)  -- super call
+  session.ident       = random_string()
+  session.csrf_secret = random_string()
+  session:save() 
+  return session
+end
+
+function Session:by_ident(ident)
+  local selector = self:new_selector()
+  selector:add_where{ 'ident = ?', ident }
+  selector:optional_object_mode()
+  return selector:exec()
+end

demo-app/model/tempstore.lua

+Tempstore = mondelefant.new_class()
+Tempstore.table = 'tempstore'
+
+function Tempstore:by_key(key)
+  local selector = self:new_selector()
+  selector:add_where{ 'key = ?', key }
+  selector:optional_object_mode()
+  return selector:exec()
+end
+
+function Tempstore:data_by_key(key)
+  local tempstore = Tempstore:by_key(key)
+  if tempstore then
+    tempstore:destroy()
+    return tempstore.data
+  end
+end
+
+function Tempstore:create(data)
+  tempstore = Tempstore:new()
+  tempstore.key = multirand.string(22, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
+  tempstore.data = data
+  tempstore:save()
+  return tempstore.key
+end

demo-app/model/track.lua

+Track = mondelefant.new_class()
+Track.table = 'track'
+
+Track:add_reference{
+  mode          = 'm1',         -- many (m) Tracks can refer to one (1) Medium
+  to            = "Medium",     -- name of referenced model (quoting avoids auto-loading of model here)
+  this_key      = 'medium_id',  -- our key in the track table
+  that_key      = 'id',         -- other key in the medium table
+  ref           = 'medium',     -- name of reference
+  back_ref      = nil,          -- not used for m1 relation!
+  default_order = nil           -- not used for m1 relation!
+}

demo-app/model/user.lua

+User = mondelefant.new_class()
+User.table = 'user'
+
+User:add_reference{
+  mode          = '1m',        -- one (1) user can have many (m) sessions
+  to            = "Session",   -- referenced model (quoting avoids auto-loading here)
+  this_key      = 'id',        -- own key in user table
+  that_key      = 'user_id',   -- other key in session table
+  ref           = 'sessions',  -- name of reference
+  back_ref      = 'user',      -- each autoloaded Session automatically refers back to the User
+  default_order = '"ident"'    -- order sessions by SQL expression "ident"
+}
+
+function User:by_ident_and_password(ident, password)
+  local selector = self:new_selector()
+  selector:add_where{ 'ident = ? AND password = ?', ident, password }
+  selector:optional_object_mode()
+  return selector:exec()
+end
+
+function User.object_get:name_with_login()
+  return self.name .. ' (' .. self.login .. ')'
+end
+
+function User.object:require_privilege(privilege)
+  if privilege == "admin" then
+    assert(self.admin, "Administrator privilege required")
+  elseif privilege == "write" then
+    assert(self.write_priv, "Write privilege required")
+  else
+    error("Unknown privilege passed to require_privilege method of User")
+  end
+end

demo-app/static/lang/de.png

Added
New image

demo-app/static/lang/en.png

Added
New image

demo-app/static/lang/es.png

Added
New image

demo-app/static/logo.png

Added
New image

demo-app/static/style.css

+/*
+ * ********** body ***********
+ */
+
+body {
+	background-color: #ddd;
+	font-family: sans-serif;
+	margin: 0;
+	padding: 0;
+	font-size: 10pt;
+}
+
+body, td, th, input, select {
+	font-size: 10pt;
+}
+
+div {
+	padding: 0px;
+	margin: 0px;
+}
+
+/*
+ * ********** Logo ***********
+ */
+
+.layout_logo {
+	background: #fff;