This commit is contained in:
darthsandmann
2016-10-16 21:53:15 +02:00
parent 0d10f8b9dc
commit c9f3117da1
412 changed files with 137942 additions and 0 deletions

View File

@ -0,0 +1,3 @@
# coding: utf-8
# hier können Paket-Initialisierungen eingetragen werden

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,161 @@
# coding: utf-8
import json
import cherrypy
from app import database
from app import view
# Method-Dispatching!
# Übersicht Anforderungen / Methoden
# (beachte: / relativ zu /lit, siehe Konfiguration Server!)
"""
Anforderung GET PUT POST DELETE
----------------------------------------------------------------
/ Liste - - -
Literatur
liefern
/0 Dokument Dokument - -
mit id=0 anlegen
liefern
(Vorgabe-Werte)
/{id} Dokument - Dokument Dokument
mit {id} ändern löschen
liefern
id > 0 !
"""
#----------------------------------------------------------
class Application_cl(object):
#----------------------------------------------------------
exposed = True # gilt für alle Methoden
#-------------------------------------------------------
def __init__(self):
#-------------------------------------------------------
# spezielle Initialisierung können hier eingetragen werden
self.db_o = database.Database_cl()
self.view_o = view.View_cl()
# es wird keine index-Methode vorgesehen, weil stattdessen
# die Webseite index.html ausgeliefert wird (siehe Konfiguration)
#-------------------------------------------------------
def GET(self, id=None):
#-------------------------------------------------------
retVal_o = {
'data': None
}
if id == None:
# Anforderung der Liste
retVal_o['data'] = self.getList_p()
else:
# Anforderung eines Dokuments
retVal_o['data'] = self.getForm_p(id)
if retVal_o['data'] == None:
cherrypy.response.status = 404
return json.dumps(retVal_o)
#-------------------------------------------------------
def PUT(self, **data_opl):
#-------------------------------------------------------
retVal_o = {
'id': None
}
# data_opl: Dictionary mit den gelieferten key-value-Paaren
# hier müsste man prüfen, ob die Daten korrekt vorliegen!
data_o = {
'name': data_opl["name_s"],
'typ': data_opl["typ_s"],
'referenz': data_opl["referenz_s"]
}
# Create-Operation
id_s = self.db_o.create_px(data_o)
retVal_o['id'] = id_s
if id_s == None:
cherrypy.response.status = 409
return json.dumps(retVal_o)
#-------------------------------------------------------
def POST(self, id, **data_opl):
#-------------------------------------------------------
# Sichern der Daten: jetzt wird keine vollständige Seite
# zurückgeliefert, sondern nur noch die Information, ob das
# Speichern erfolgreich war
retVal_o = {
'id': None
}
# data_opl: Dictionary mit den gelieferten key-value-Paaren
# hier müsste man prüfen, ob die Daten korrekt vorliegen!
id_s = data_opl["id_s"]
data_o = {
'name': data_opl["name_s"],
'typ': data_opl["typ_s"],
'referenz': data_opl["referenz_s"]
}
# Update-Operation
retVal_o['id'] = id_s
if self.db_o.update_px(id_s, data_o):
pass
else:
cherrypy.response.status = 404
return json.dumps(retVal_o)
#-------------------------------------------------------
def DELETE(self, id):
#-------------------------------------------------------
# Eintrag löschen, nur noch Rückmeldung liefern
retVal_o = {
'id': id
}
if self.db_o.delete_px(id):
pass
else:
cherrypy.response.status = 404
return json.dumps(retVal_o)
#-------------------------------------------------------
def default(self, *arguments, **kwargs):
#-------------------------------------------------------
msg_s = "unbekannte Anforderung: " + \
str(arguments) + \
' ' + \
str(kwargs)
raise cherrypy.HTTPError(404, msg_s)
#-------------------------------------------------------
def getList_p(self):
#-------------------------------------------------------
data_o = self.db_o.read_px()
return self.view_o.createList_px(data_o)
#-------------------------------------------------------
def getForm_p(self, id_spl):
#-------------------------------------------------------
data_o = self.db_o.read_px(id_spl)
if data_o != None:
return self.view_o.createForm_px(id_spl, data_o)
else:
return None
# EOF

View File

@ -0,0 +1,125 @@
# coding: utf-8
import os
import os.path
import codecs
import json
#----------------------------------------------------------
class Database_cl(object):
#----------------------------------------------------------
# Daten in dieser Variante dauerhaft (persistent) speichern
# dazu jedes Element in einer Datei, die entsprechend der id benannt ist, speichern
# alle Elemente werden zur Laufzeit des Servers zur Vereinfachung auch im
# Hauptspeicher abgelegt
# die nächste zu vergebende Id wird ebenfalls dauerhaft gespeichert
# zur Vereinfachung wird hier fest vorgegebenen, dass sich die Daten
# im Unterverzeichnis "data" befinden
# es wird ferner angenommen, dass die Datei "data/maxid.dat" bereits existiert
# und als einzigen Eintrag den aktuellen Wert der maximalen Id enthält
#-------------------------------------------------------
def __init__(self):
#-------------------------------------------------------
self.data_o = {}
self.readData_p()
#-------------------------------------------------------
def create_px(self, data_opl):
#-------------------------------------------------------
# Überprüfung der Daten müsste ergänzt werden!
id_s = self.nextId_p()
# Datei erzeugen
file_o = codecs.open(os.path.join('data', id_s+'.dat'), 'w', 'utf-8')
file_o.write(json.dumps(data_opl, indent=3, ensure_ascii=True))
file_o.close()
self.data_o[id_s] = data_opl
return id_s
#-------------------------------------------------------
def read_px(self, id_spl = None):
#-------------------------------------------------------
# hier zur Vereinfachung:
# Aufruf ohne id: alle Einträge liefern
data_o = None
if id_spl == None:
data_o = self.data_o
elif id_spl == '0':
data_o = self.getDefault_px()
else:
if id_spl in self.data_o:
data_o = self.data_o[id_spl]
return data_o
#-------------------------------------------------------
def update_px(self, id_spl, data_opl):
#-------------------------------------------------------
# Überprüfung der Daten müsste ergänzt werden!
status_b = False
if id_spl in self.data_o:
# Datei aktualisieren
file_o = codecs.open(os.path.join('data', id_spl+'.dat'), 'w', 'utf-8')
file_o.write(json.dumps(data_opl, indent=3, ensure_ascii=True))
file_o.close()
self.data_o[id_spl] = data_opl
status_b = True
return status_b
#-------------------------------------------------------
def delete_px(self, id_spl):
#-------------------------------------------------------
status_b = False
if id_spl in self.data_o:
# Datei entfernen
os.remove(os.path.join('data', id_spl+'.dat'))
del self.data_o[id_spl]
status_b = True
return status_b
#-------------------------------------------------------
def getDefault_px(self):
#-------------------------------------------------------
return {
'name': '',
'typ': 'Typ1',
'referenz': ''
}
#-------------------------------------------------------
def readData_p(self):
#-------------------------------------------------------
files_a = os.listdir('data')
for fileName_s in files_a:
if fileName_s.endswith('.dat') and fileName_s != 'maxid.dat':
file_o = codecs.open(os.path.join('data', fileName_s), 'rU', 'utf-8')
content_s = file_o.read()
file_o.close()
id_s = fileName_s[:-4]
self.data_o[id_s] = json.loads(content_s)
#-------------------------------------------------------
def nextId_p(self):
#-------------------------------------------------------
file_o = open(os.path.join('data', 'maxid.dat'), 'r+')
maxId_s = file_o.read()
maxId_s = str(int(maxId_s)+1)
file_o.seek(0)
file_o.write(maxId_s)
file_o.close()
return maxId_s
# EOF

View File

@ -0,0 +1,52 @@
# coding: utf-8
import json
import os
import codecs
import cherrypy
# Method-Dispatching!
# Übersicht Anforderungen / Methoden
# (beachte: / relativ zu /template, siehe Konfiguration Server!)
"""
Anforderung GET PUT POST DELETE
----------------------------------------------------------------
/ Alle - - -
Templates
liefern
"""
#----------------------------------------------------------
class Template_cl(object):
#----------------------------------------------------------
exposed = True # gilt für alle Methoden
#-------------------------------------------------------
def __init__(self):
#-------------------------------------------------------
pass
#-------------------------------------------------------
def GET(self):
#-------------------------------------------------------
retVal_o = {
'templates': {}
}
files_a = os.listdir('templates')
for fileName_s in files_a:
file_o = codecs.open(os.path.join('templates', fileName_s), 'rU', 'utf-8')
content_s = file_o.read()
file_o.close()
retVal_o["templates"][fileName_s] = content_s
return json.dumps(retVal_o)
# EOF

View File

@ -0,0 +1,32 @@
# coding: utf-8
#----------------------------------------------------------
class View_cl(object):
#----------------------------------------------------------
#-------------------------------------------------------
def __init__(self):
#-------------------------------------------------------
pass
#-------------------------------------------------------
def createList_px(self, data_opl):
#-------------------------------------------------------
return data_opl
#-------------------------------------------------------
def createForm_px(self, id_spl, data_opl):
#-------------------------------------------------------
if id_spl == None:
data_opl['id'] = ''
elif id_spl == '':
data_opl['id'] = ''
elif id_spl == '0':
data_opl['id'] = ''
else:
data_opl['id'] = id_spl
return data_opl
# EOF

194
Sammlung/lit-8/css/main.css Normal file
View File

@ -0,0 +1,194 @@
/* main.css */
/* allgemeine Vorgaben */
body {
font-family: "Open Sans", sans-serif;
font-size: 12pt;
padding: 0;
margin: 0;
}
/* Basislayout */
.clSiteHeader {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50px;
line-height: 50px;
margin: 0;
padding: 5px;
font-size: 12pt;
color: white;
background-color: #0000AA;
}
.clNav {
position: absolute;
top: 60px; /* height, padding, border, margin von idSiteHeader beachten */
left: 0;
bottom: 0;
width: 140px;
border-right: 1px solid;
margin: 0;
padding: 5px;
}
.clNav a, a:hover, a:visited, a:active{
display: block;
text-decoration: none;
margin-bottom: 10px;
}
.clNav a:hover {
font-weight: bold;
}
.clContentOuter {
position: absolute;
top: 60px; /* height, padding, border, margin von idSiteHeader beachten */
left: 151px;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
}
.clContent {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 5px;
}
/* Elemente im Content-Bereich */
.clContentHeader {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 30px;
line-height: 30px;
margin: 0;
padding: 5px;
font-size: 14pt;
text-align: center;
}
.clContentArea {
position: absolute;
top: 40px; /* height, padding, border, margin von idContentHeader beachten */
left: 0;
right: 0;
bottom: 40px; /* height, padding, border, margin von idButtonArea beachten */
margin: 0;
padding: 0px;
overflow-y: auto;
}
.clButtonArea {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 30px;
line-height: 30px;
margin: 0;
padding: 5px;
text-align: center;
background-color: #EEEEEE;
}
/* Links und Submit-Schalter im Buttonbereich gestalten */
.clButtonArea button, a, input[type="submit"] {
margin: 0 5px;
padding: 3px 6px;
font-size: 10pt;
text-decoration: none;
border: 1px solid;
color: black;
background-color: buttonface;
}
.clButtonArea button:disabled {
color: graytext;
}
/* unterschiedliche Kennzeichnungen je nach Bedienung vermeiden */
.clButtonArea a:hover, a:visited, a:active {
color: black;
}
/* Gestaltung von Tabellen */
#idList {
table-layout: fixed;
width: 80%;
border: 1px solid;
border-collapse: collapse;
margin:auto;
}
#idList th {
text-align: left;
padding-left: 6px;
}
#idList th, #idList td {
padding: 3px;
border: 1px solid;
}
/* damit wird jede ungerade Zeile in Tabellen mit einer anderen Hintergrundfarbe ausgegeben */
/* den Effekt nennt man auch "Zebra"-Tables */
tr:nth-of-type(odd) {
background-color:#ccc;
}
#idList tr.clSelected {
background-color: blue;
color: white;
}
/* Gestaltung von Formularen */
/* das Formular nochmals zusätzlich gestalten */
#idForm .clContentArea {
width: 500px;
margin: auto;
}
.clFormRow {
position: relative; /* damit das Element in jedem Fall "positioniert" ist und damit als Bezugspunkt geeignet ist */
height: 25px;
margin-bottom: 10px;
}
.clFormRow label {
position: absolute;
top: 0;
left: 0;
width: 150px;
text-align: right;
}
.clFormRow input {
position: absolute;
top: 0;
left: 160px;
width: 250px;
}
label span.clRequired {
color: red;
content: '*';
}
/* EOF */

View File

@ -0,0 +1,5 @@
{
"typ": "Typ1",
"name": "x",
"referenz": ""
}

View File

@ -0,0 +1,5 @@
{
"typ": "Typ2",
"name": "y",
"referenz": ""
}

View File

@ -0,0 +1,5 @@
{
"typ": "Typ1",
"name": "z",
"referenz": ""
}

View File

@ -0,0 +1,5 @@
{
"typ": "Typ2",
"name": "zzz123",
"referenz": "http://www.xyz.de"
}

View File

@ -0,0 +1,5 @@
{
"typ": "Typ1",
"name": "xywwww",
"referenz": "http://123.de"
}

View File

@ -0,0 +1,5 @@
{
"name": "test",
"typ": "Typ1",
"referenz": "test2"
}

View File

@ -0,0 +1,5 @@
{
"typ": "Typ2",
"name": "3namexxxx",
"referenz": "http://"
}

View File

@ -0,0 +1 @@
37

View File

@ -0,0 +1,26 @@
<form id="idForm" class="clContent" action="/save" method="POST">
<h2 id="idContentHeader" class="clContentHeader">
Literatur- / Quellenverzeichnis / Formular
</h2>
<div id="idContentArea" class="clContentArea">
<input type="hidden" value="" id="id_s" name="id_s" />
<div class="clFormRow">
<label for="name_s">Name <span class="clRequired"></span></label>
<input type="text" value="" id="name_s" name="name_s" autofocus required />
</div>
<div class="clFormRow">
<label for="typ_s">Typ <span class="clRequired"></span></label>
<input type="text" value="" id="typ_s" name="typ_s" required />
</div>
<div class="clFormRow">
<label for="referenz_s">Referenz</label>
<input type="url" value="" id="referenz_s" name="referenz_s" />
</div>
</div>
<div id="idButtonArea" class="clButtonArea">
<button data-action="back" class="clButton">Zurück</button>
<button data-action="save" class="clButton">Speichern</button>
</div>
</form>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>
Web Engineering 2013/2014 / Beispiel lit-8
</title>
<meta charset="UTF-8" />
<style type="text/css">
@import url("/css/main.css");
</style>
<script type="text/javascript" src="/js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="/js/inheritance.js"></script>
<script type="text/javascript" src="/js/es.js"></script>
<script type="text/javascript" src="/js/te.js"></script>
<script type="text/javascript" src="/js/tm.js"></script>
<!-- hier zur Verdeutlichung der Strukturierung Aufteilung in mehrere js-Quellen -->
<!-- wird man im produktiven Einsatz zu einer js-Quelle zusammenfassen -->
<script type="text/javascript" src="/js/lit.js"></script>
<script type="text/javascript" src="/js/litform.js"></script>
<script type="text/javascript" src="/js/litlist.js"></script>
<script type="text/javascript" src="/js/litnav.js"></script>
</head>
<body>
<header>
<h1 id="idSiteHeader" class="clSiteHeader">
Web-Engineering: Anwendungsbeispiel Lit-8
</h1>
</header>
<section id="idContentOuter" class="clContentOuter">
</section>
<nav id="idNav" class="clNav">
</nav>
</body>
</html>

View File

@ -0,0 +1,16 @@
<div id="idListContent" class="clContent">
<h2 id="idContentHeader" class="clContentHeader">
Literatur- / Quellenverzeichnis
</h2>
<div id="idContentArea" class="clContentArea">
<table id="idList">
<tr class="listheader"><th>Name</th><th>Typ</th><th>Referenz</th></tr>
</table>
</div>
<div id="idButtonArea" class="clButtonArea">
<button data-action="add" class="clButton">Hinzufügen</button>
<button data-action="edit" class="clButton">Bearbeiten</button>
<button data-action="delete" class="clButton">Löschen</button>
</div>
</div>

91
Sammlung/lit-8/js/es.js Normal file
View File

@ -0,0 +1,91 @@
//------------------------------------------------------------------------------
// Event-Service: asynchroner Nachrichtenaustausch
//------------------------------------------------------------------------------
// depends-on:
// jquery
// inheritance
// underscore
//------------------------------------------------------------------------------
function defer_p (notifier_ppl, entry_opl, message_spl, data_opl) {
return setTimeout(function() {
return notifier_ppl.apply(entry_opl, [entry_opl, message_spl, data_opl]);
}, 1);
}
function each(object_opl, iterator, context) {
for (var key in object_opl) {
iterator.call(context, object_opl[key], key);
}
}
function findAll(object_opl, iterator, context) {
var results = [];
each(object_opl, function(value, index) {
if (iterator.call(context, value, index))
results.push(value);
});
return results;
}
function compact(object_opl) {
return findAll(object_opl, function(value) {
return value != null;
});
}
//------------------------------------------------------------------------------
var EventService_cl = Class.create({
//------------------------------------------------------------------------------
initialize: function () {
this.Data_o = null;
this.Subscriber_o = {};
this.Method_o = null;
},
subscribe_px: function (Subscriber_opl, Message_spl) {
if (Message_spl in this.Subscriber_o) {
// Message bekannt, Liste der Subscriber untersuchen
if (this.Subscriber_o[Message_spl].indexOf(Subscriber_opl) == -1) {
this.Subscriber_o[Message_spl].push(Subscriber_opl);
}
} else {
// Message noch nicht vorhanden, neu eintragen
this.Subscriber_o[Message_spl] = [Subscriber_opl];
}
},
unSubscribe_px: function (Subscriber_opl, Message_spl) {
if (Message_spl in this.Subscriber_o) {
// Message bekannt, Liste der Subscriber untersuchen
var Entry_a = this.Subscriber_o[Message_spl];
var index_i = Entry_a.indexOf(Subscriber_opl);
if (index_i >= 0) {
// Eintrag entfernen
Entry_a[index_i] = null;
Entry_a = compact(Entry_a); // compact liefert Kopie!
if (Entry_a.length == 0) {
// keine Subscriber mehr, kann entfernt werden
delete this.Subscriber_o[Message_spl];
}
}
} else {
// Message nicht vorhanden, falsche Anforderung
}
},
publish_px: function (Message_spl, Data_opl) {
console.info('es - publish ' + Message_spl);
each(this.Subscriber_o, function (value_apl, key_spl) {
// geliefert wird jeweils ein Wert, hier ein Array, und der Key
if (key_spl == Message_spl) {
// an alle Subscriber weitergeben
each(value_apl, function (entry_opl, index_ipl) {
// geliefert wird hier das Element und der Index
//_.defer(entry_opl.notify_px, entry_opl, Message_spl, Data_opl);
defer_p(entry_opl.notify_px, entry_opl, Message_spl, Data_opl);
}, this
);
}
}, this
)
}
});
// EOF

View File

@ -0,0 +1,122 @@
/*
Class, version 2.7
Copyright (c) 2006, 2007, 2008, Alex Arnell <alex@twologic.com>
Licensed under the new BSD License. See end of file for full license terms.
*/
var Class = (function() {
var __extending = {};
return {
extend: function(parent, def) {
if (arguments.length == 1) { def = parent; parent = null; }
var func = function() {
if (arguments[0] == __extending) { return; }
this.initialize.apply(this, arguments);
};
if (typeof(parent) == 'function') {
func.prototype = new parent( __extending);
}
var mixins = [];
if (def && def.include) {
if (def.include.reverse) {
// methods defined in later mixins should override prior
mixins = mixins.concat(def.include.reverse());
} else {
mixins.push(def.include);
}
delete def.include; // clean syntax sugar
}
if (def) Class.inherit(func.prototype, def);
for (var i = 0; (mixin = mixins[i]); i++) {
Class.mixin(func.prototype, mixin);
}
return func;
},
mixin: function (dest, src, clobber) {
clobber = clobber || false;
if (typeof(src) != 'undefined' && src !== null) {
for (var prop in src) {
if (clobber || (!dest[prop] && typeof(src[prop]) == 'function')) {
dest[prop] = src[prop];
}
}
}
return dest;
},
inherit: function(dest, src, fname) {
if (arguments.length == 3) {
var ancestor = dest[fname], descendent = src[fname], method = descendent;
descendent = function() {
var ref = this.parent; this.parent = ancestor;
var result = method.apply(this, arguments);
ref ? this.parent = ref : delete this.parent;
return result;
};
// mask the underlying method
descendent.valueOf = function() { return method; };
descendent.toString = function() { return method.toString(); };
dest[fname] = descendent;
} else {
for (var prop in src) {
if (dest[prop] && typeof(src[prop]) == 'function') {
Class.inherit(dest, src, prop);
} else {
dest[prop] = src[prop];
}
}
}
return dest;
},
singleton: function() {
var args = arguments;
if (args.length == 2 && args[0].getInstance) {
var klass = args[0].getInstance(__extending);
// we're extending a singleton swap it out for it's class
if (klass) { args[0] = klass; }
}
return (function(args){
// store instance and class in private variables
var instance = false;
var klass = Class.extend.apply(args.callee, args);
return {
getInstance: function () {
if (arguments[0] == __extending) return klass;
if (instance) return instance;
return (instance = new klass());
}
};
})(args);
}
};
})();
// finally remap Class.create for backward compatability with prototype
Class.create = function() {
return Class.extend.apply(this, arguments);
};
/*
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
* Neither the name of typicalnoise.com nor the names of its contributors may be
used to endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

9597
Sammlung/lit-8/js/jquery-1.9.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

92
Sammlung/lit-8/js/lit.js Normal file
View File

@ -0,0 +1,92 @@
// ----------------------------------------------
// Beispiel lit-8
// lit.js
// ----------------------------------------------
// Verwendung von jquery, inheritance, Single-Page / Ajax, Event-Service
// REST-Interface
// ----------------------------------------------
// ----------------------------------------------
// Namensraum einrichten
// ----------------------------------------------
var LITAPP = {};
// ----------------------------------------------
LITAPP.Application_cl = Class.create({
// ----------------------------------------------
initialize: function () {
this.content_o = null; // das jeweils aktuelle Objekt im Contentbereich
this.nav_o = new LITAPP.Nav_cl();
this.listView_o = new LITAPP.ListView_cl();
this.detailView_o = new LITAPP.DetailView_cl();
// Registrierungen
LITAPP.es_o.subscribe_px(this, 'app');
},
notify_px: function (self_opl, message_spl, data_apl) {
switch (message_spl) {
case 'app':
switch (data_apl[0]) {
case 'init':
LITAPP.tm_o = new TELIB.TemplateManager_cl();
break;
case 'templates.loaded':
// Liste im Content-Bereich anzeigen
self_opl.setContent_p(self_opl.listView_o, data_apl[1]);
break;
case 'list':
// Liste im Content-Bereich anzeigen
self_opl.setContent_p(self_opl.listView_o, data_apl[1]);
break;
case 'add':
// (leeres) Detailformular im Content-Bereich anzeigen
self_opl.setContent_p(self_opl.detailView_o, data_apl[1]);
break;
case 'edit':
// Detailformular im Content-Bereich anzeigen
self_opl.setContent_p(self_opl.detailView_o, data_apl[1]);
break;
case 'back':
// Detailformular wird verlassen, Liste im Content-Bereich anzeigen
self_opl.setContent_p(self_opl.listView_o, data_apl[1]);
break;
default:
console.warning('[Application_cl] unbekannte app-Notification: '+data_apl[0]);
break;
}
break;
default:
console.warning('[Application_cl] unbekannte Notification: '+message_spl);
break;
}
},
setContent_p: function (newContent_opl, data_opl) {
if (this.content_o != null) {
if (this.content_o === newContent_opl) {
// wird bereits angezeigt, keine Änderung
} else {
if (this.content_o.canClose_px()) {
this.content_o.close_px();
this.content_o = newContent_opl;
this.content_o.render_px(data_opl);
}
}
} else {
this.content_o = newContent_opl;
this.content_o.render_px(data_opl);
}
}
});
// ----------------------------------------------
$(document).ready(function(){
// ----------------------------------------------
// wird ausgeführt, wenn das Dokument vollständig geladen wurde
LITAPP.es_o = new EventService_cl();
LITAPP.app_o = new LITAPP.Application_cl();
LITAPP.es_o.publish_px('app', ['init', null]);
});
// EOF

View File

@ -0,0 +1,139 @@
// ----------------------------------------------
// Beispiel lit-8
// litform.js
// ----------------------------------------------
// ----------------------------------------------
LITAPP.DetailView_cl = Class.create({
// ----------------------------------------------
initialize: function () {
// Basis-Markup des Formulars anfordern
var that = this;
$.get('/html/detail.html', function (data_spl) {
$("#idContentOuter").append(data_spl);
$("#idForm").hide();
that.initHandler_p();
that.storeFormContent_p();
});
},
canClose_px: function () {
// Prüfen, ob Formularinhalt verändert wurde
var mod_b = this.isModified_p();
if (mod_b) {
if (confirm("Es gibt nicht gespeicherte Änderungen - verwerfen?")) {
mod_b = false;
}
}
return !mod_b;
},
close_px: function () {
$("#idForm").hide();
},
render_px: function (data_opl) {
var path_s;
if (data_opl != null) {
path_s = '/lit/' + data_opl;
} else {
path_s = '/lit/0';
}
$.ajax({
dataType: "json",
url: path_s,
type: 'GET'
})
.done($.proxy(this.doRender_p, this))
.fail(function(jqXHR_opl, textStatus_spl) {
alert( "[Form] Fehler bei Anforderung: " + textStatus_spl );
});
},
doRender_p: function (data_opl) {
// in das Formular übertragen
var data_o = data_opl['data'];
$('#id_s').val(data_o['id']);
$('#name_s').val(data_o['name'])
$('#typ_s').val(data_o['typ']);
$('#referenz_s').val(data_o['referenz']);
this.storeFormContent_p();
$("#idForm").show();
},
initHandler_p: function () {
// Ereignisverarbeitung für das Formular einrichten
$("#idForm").on("click", "button", $.proxy(this.onClickButtons_p, this));
},
onClickButtons_p: function (event_opl) {
var do_b = false;
var path_s;
var action_s = $(event_opl.target).attr("data-action");
switch (action_s) {
case "back":
// Weiterleiten
LITAPP.es_o.publish_px('app', [action_s, null]);
break;
case "save":
// Formularinhalt prüfen
if (this.isModified_p()) {
if (this.checkContent_p()) {
// kein klassisches submit, es wird auch keine neue Anzeige vorgenommen
var path_s = '/lit';
var data_s = $("#idForm").serialize();
var type_s = 'POST';
var id_s = $('#id_s').val();
if (id_s == '') {
type_s = 'PUT';
} else {
path_s += '/' + id_s;
}
//var that = this;
$.ajax({
context: this,
dataType: "json",
data: data_s,
url: path_s,
type: type_s
})
.done(function (data_opl) {
// Umwandlung der JSON-Daten vom Server bereits erfolgt
$('#id_s').val(data_opl['id']);
// aktuellen Formularinhalt speichern
// (das Formular wird ja nicht mehr neu geladen!)
this.storeFormContent_p();
alert("Speichern ausgeführt!");
})
.fail(function(jqXHR_opl, textStatus_spl) {
alert( "Fehler bei Anforderung: " + textStatus_spl );
});
} else {
alert("Bitte prüfen Sie die Eingaben in den Formularfeldern!")
}
}
break;
}
// Weiterleitung und Standardbearbeitung unterbinden
event_opl.stopPropagation();
event_opl.preventDefault();
},
isModified_p: function () {
// Prüfen, ob Formularinhalt verändert wurde
var mod_b = this.FormContentOrg_s != $("#idForm").serialize();
return mod_b;
},
checkContent_p: function () {
// hier nur zur Demonstration Prüfung des Typs gegen eine Werteliste
// (das realisiert man besser mit einer Liste)
var status_b = true;
var typ_s = $("#typ_s").val();
if ((typ_s != "Typ1") && (typ_s != "Typ2")) {
status_b = false;
}
return status_b;
},
storeFormContent_p: function () {
this.FormContentOrg_s = $("#idForm").serialize();
}
});
// EOF

View File

@ -0,0 +1,168 @@
// ----------------------------------------------
// Beispiel lit-8
// litlist.js
// ----------------------------------------------
// ----------------------------------------------
LITAPP.ListView_cl = Class.create({
// ----------------------------------------------
initialize: function () {
var that = this;
// Basis-Markup der Tabelle anfordern
$.get('/html/list.html', function (data_spl) {
$("#idContentOuter").append(data_spl);
$("#idListContent").hide();
that.initHandler_p();
that.initList_p();
});
// Registrierungen
LITAPP.es_o.subscribe_px(this, 'list');
},
notify_px: function (self_opl, message_spl, data_apl) {
switch (message_spl) {
case 'list':
switch (data_apl[0]) {
case 'refresh':
// Liste aktualisieren
self_opl.render_px(null);
break;
default:
console.warning('[ListView_cl] unbekannte list-Notification: '+data_apl[0]);
break;
}
break;
default:
console.warning('[ListView_cl] unbekannte Notification: '+message_spl);
break;
}
},
canClose_px: function () {
return true;
},
close_px: function () {
$("#idListContent").hide();
},
render_px: function (data_opl) {
// Parameter data_opl wird hier nicht benötigt
// Anforderung an den Server senden
$.ajax({
dataType: "json",
url: '/lit',
type: 'GET'
})
.done($.proxy(this.doRender_p, this))
.fail(function(jqXHR_opl, textStatus_spl) {
alert( "[Liste] Fehler bei Anforderung: " + textStatus_spl );
});
},
doRender_p: function (data_opl) {
// json-Daten bereits in js-Objekte umgesetzt
var rows_s = LITAPP.tm_o.execute_px('list.tpl', data_opl);
this.initList_p();
$("#idList tr[class!='listheader']").remove();
$("#idList").append(rows_s);
$("#idListContent").show();
console.log("[ListView_cl] doRender");
},
initList_p: function () {
this.rowId_s = ""; // id der selektierten Zeile
// Buttons teilweise deaktivieren, bis eine Zeile ausgewählt wurde
this.disableButtons_p();
},
initHandler_p: function () {
// Ereignisverarbeitung einrichten
// Ereignisverarbeitung für die Tabelle einrichten
// man beachte: für jquery muss man CSS-Selektoren angeben, also #idList statt einfach nur idList !
$("#idList").on("click", "td", $.proxy(this.onClickList_p, this));
// Ereignisverarbeitung für die Schalter einrichten
// stärkere Einschränkung mit #idListContent notwendig, damit keine Verwechslung mit ButtonArea auf Form erfolgt
$("#idListContent #idButtonArea").on("click", "button", $.proxy(this.onClickButtons_p, this));
},
onClickList_p: function (event_opl) {
// hier werden nur click-Events auf td-Elemente geliefert
if (this.rowId_s != "") {
$("#"+this.rowId_s).removeClass("clSelected"); // Achtung: jetzt ist nur die Bezeichnung der CSS-Klasse gemeint!
}
this.rowId_s = $(event_opl.target).parent().attr('id');
$("#"+this.rowId_s).addClass("clSelected");
this.enableButtons_p();
},
onClickButtons_p: function (event_opl) {
var action_s = $(event_opl.target).attr("data-action");
switch (action_s) {
case 'add':
// weiterleiten
LITAPP.es_o.publish_px('app', [action_s, null]);
break;
case 'edit':
if (this.rowId_s != "") {
// Weiterleiten
LITAPP.es_o.publish_px('app', [action_s, this.rowId_s]);
} else {
alert("Wählen Sie bitte einen Eintrag in der Tabelle aus!");
}
break;
case 'delete':
if (this.rowId_s != "") {
if (confirm("Soll der Datensatz gelöscht werden?")) {
// Id der selektierten Tabellenzeile anhängen
var path_s = "/lit/" + this.rowId_s;
$.ajax({
context: this,
dataType: "json",
url: path_s,
type: 'DELETE'
})
.done(function (data_opl) {
// Auswertung der Rückmeldung
// der umständliche Weg:
// - Liste neu darstellen, hier vereinfacht durch neue Anforderung
//LITAPP.es_o.publish_px('list', ['refresh', null]);
// einfacher mit direktem Entfernen der Zeile aus der Tabelle
// (id des gelöschten Eintrags wird in der Antwort geliefert)
$('#'+data_opl['id']).remove();
this.initList_p();
})
.fail(function(jqXHR_opl, textStatus_spl) {
alert( "[Liste] Fehler bei Anforderung: " + textStatus_spl );
});
}
} else {
alert("Wählen Sie bitte einen Eintrag in der Tabelle aus!");
}
break;
}
// Weiterleitung und Standardbearbeitung unterbinden
event_opl.stopPropagation();
event_opl.preventDefault();
},
// stärkere Einschränkung mit #idListContent notwendig, damit nicht die Buttons auf dem
// Formular ebenfalls geändert werden
enableButtons_p: function () {
$("#idListContent #idButtonArea button").each(function () {
if ($(this).attr("data-action") != "add") {
$(this).prop("disabled", false);
}
});
},
disableButtons_p: function () {
$("#idListContent #idButtonArea button").each(function () {
if ($(this).attr("data-action") != "add") {
$(this).prop("disabled", true);
}
});
}
});
// EOF

View File

@ -0,0 +1,37 @@
// ----------------------------------------------
// Beispiel lit-8
// litnav.js
// ----------------------------------------------
// ----------------------------------------------
LITAPP.Nav_cl = Class.create({
// ----------------------------------------------
initialize: function () {
// zur Vereinfachung hier direkt den Inhalt des
// Navigationsbereichs anzeigen und die Ereignisverarbeitung einrichten
this.render_px();
this.initHandler_p();
},
render_px: function (data_opl) {
// Parameter data_opl wird hier nicht benötigt
// feste Voragben berücksichtigen
var markup_s = '<a href="#" data-action="list">Liste</a> ' +
'<a href="#" data-action="add">Hinzufügen</a>';
$('#idNav').html(markup_s);
},
initHandler_p: function () {
// Ereignisverarbeitung für die Schalter einrichten
$("#idNav").on("click", "a", function (event_opl) {
var action_s = $(event_opl.target).attr('data-action');
// Weiterleitung! Das Nav-Objekt ist nicht für die Bearbeitung direkt verantwortlich
LITAPP.es_o.publish_px('app', [action_s, null]);
// Weiterleitung und Standardbearbeitung unterbinden
event_opl.stopPropagation();
event_opl.preventDefault();
});
}
});
// EOF

308
Sammlung/lit-8/js/te.js Normal file
View File

@ -0,0 +1,308 @@
//------------------------------------------------------------------------------
// Template-Engine
//------------------------------------------------------------------------------
// depends-on:
// inheritance
//------------------------------------------------------------------------------
// String-Methoden ergänzen
APPCO = {};
APPCO.apply = function(o, c, defaults){
// no "this" reference for friendly out of scope calls
if (defaults) {
APPCO.apply(o, defaults);
}
if (o && c && typeof c == 'object') {
for (var p in c) {
o[p] = c[p];
}
}
return o;
};
// quick and dirty! Manche Autoren lehnen solche Erweiterungen ab
APPCO.apply(String.prototype, {
include: function (pattern) {
return this.indexOf(pattern) > -1;
},
startsWith: function (pattern) {
return this.lastIndexOf(pattern, 0) === 0;
},
endsWith: function (pattern) {
var d = this.length - pattern.length;
return d >= 0 && this.indexOf(pattern, d) === d;
}
});
APPCO.apply(String, {
interpret: function(value) {
return value == null ? '' : String(value);
}
});
// Namensraum
var TELIB = {};
TELIB.Generator_cl = Class.create({
initialize: function () {
this.reset_px();
},
reset_px: function () {
this.code_a = ['var result_a = [];\n'];
},
write_px: function (text_spl) {
// Escape-Zeichen bei Strings ergänzen
var text_s = text_spl.replace(/"/g, '\\"');
text_s = text_s.replace(/'/g, "\\'");
this.code_a.push('result_a.push("' + text_s + '");\n');
},
code_px: function (code_spl) {
if (code_spl.startsWith('if')) {
this.code_a.push('if (' + code_spl.substr(2) + ') {\n');
} else if (code_spl.startsWith('else')) {
this.code_a.push('} else {\n');
} else if (code_spl.startsWith('endif')) {
this.code_a.push('}\n');
} else if (code_spl.startsWith('for')) {
this.code_a.push('for (' + code_spl.substr(3) + ') {\n');
} else if (code_spl.startsWith('endfor')) {
this.code_a.push('}\n');
} else {
this.code_a.push(code_spl + '\n');
}
},
substitute_px: function (subst_spl) {
this.code_a.push('result_a.push(' + String.interpret(subst_spl) + ');\n');
},
generate_px: function () {
var result_s = this.code_a.join('') + ' return result_a.join("");';
var f_o = new Function ('context', result_s);
return f_o;
}
});
TELIB.TemplateCompiler_cl = Class.create({
initialize: function () {
this.gen_o = new TELIB.Generator_cl();
this.reset_px();
},
reset_px: function () {
this.gen_o.reset_px();
this.preservePreWS_b = false;
this.preservePostWS_b = false;
},
setPreWS_px: function (on_bpl) {
if ((on_bpl == undefined) || (on_bpl == false)) {
this.preservePreWS_b = false;
} else {
this.preservePreWS_b = true;
}
},
setPostWS_px: function (on_bpl) {
if ((on_bpl == undefined) || (on_bpl == false)) {
this.preservePostWS_b = false;
} else {
this.preservePostWS_b = true;
}
},
compile_px: function (source_spl) {
var state_i = 0;
var pos_i = 0;
var len_i = source_spl.length;
var text_s = '';
var code_s = '';
var subst_s = '';
this.gen_o.reset_px();
var doubletest_f = function (char_spl) {
var status_b = false;
if ((pos_i + 1) < len_i) {
if ((source_spl[pos_i] == char_spl) && (source_spl[pos_i+1] == char_spl)) {
status_b = true;
}
}
return status_b;
}
while (pos_i < len_i) {
switch (state_i) {
case 0:
// outside
if (source_spl[pos_i] == '@') { // ECMA 5!
if (doubletest_f('@') == false) {
state_i = 2;
code_s = '';
} else {
// als normales Zeichen verarbeiten, ein Zeichen überlesen
state_i = 1;
text_s = '@';
pos_i++;
}
} else if (source_spl[pos_i] == '#') {
if (doubletest_f('#') == false) {
state_i = 3;
subst_s = '';
} else {
// als normales Zeichen verarbeiten, ein Zeichen überlesen
state_i = 1;
text_s = '#';
pos_i++;
}
} else if ((source_spl[pos_i] == ' ') || (source_spl[pos_i] == '\t')) {
state_i = 4;
text_s = '';
pos_i--; // Zeichen erneut verarbeiten
} else {
state_i = 1;
text_s = '';
pos_i--; // Zeichen erneut verarbeiten
}
break;
case 1:
// inText
if (source_spl[pos_i] == '@') {
if (doubletest_f('@') == false) {
// Textende, neuer Code
state_i = 0;
this.gen_o.write_px(text_s);
pos_i--; // Zeichen erneut verarbeiten
} else {
// als normales Zeichen verarbeiten, ein Zeichen überlesen
text_s += '@';
pos_i++;
}
} else if (source_spl[pos_i] == '#') {
if (doubletest_f('#') == false) {
// Textende, neue Substitution
state_i = 0;
this.gen_o.write_px(text_s);
pos_i--; // Zeichen erneut verarbeiten
// Textende, neuer Code
} else {
// als normales Zeichen verarbeiten, ein Zeichen überlesen
text_s += '#';
pos_i++;
}
} else if ((source_spl[pos_i] == ' ') || (source_spl[pos_i] == '\t')) {
// Textende
state_i = 0;
this.gen_o.write_px(text_s);
pos_i--; // Zeichen erneut verarbeiten
} else {
// sammeln
if ((source_spl[pos_i] != '\n') && (source_spl[pos_i] != '\r')) {
text_s += source_spl[pos_i];
} else if (source_spl[pos_i] == '\n') {
text_s += '\\n';
} else {
text_s += '\\r';
}
}
break;
case 2:
// inCode
if (source_spl[pos_i] == '@') {
if (doubletest_f('@') == false) {
// zu Ende, erzeugen
this.gen_o.code_px(code_s);
state_i = 5; //0
} else {
// als normales Zeichen verarbeiten, ein Zeichen überlesen
code_s += '@';
pos_i++;
}
} else {
// sammeln
code_s += source_spl[pos_i];
}
break;
case 3:
// inSubst
// Verdopplung # hier nicht zulässig!
if (source_spl[pos_i] == '#') {
// zu Ende, erzeugen
this.gen_o.substitute_px(subst_s);
state_i = 0;
} else {
// sammeln
subst_s += source_spl[pos_i];
}
break;
case 4:
// pre-code-whitespace
switch (source_spl[pos_i]) {
case ' ':
case '\t':
// sammeln
text_s += source_spl[pos_i];
break;
default:
state_i = 0;
if (source_spl[pos_i] != '@') {
this.gen_o.write_px(text_s);
} else {
if (doubletest_f('@') == false) {
// Whitespace vor Code-Beginn erkannt
if (this.preservePreWS_b == true) {
// trotzdem ausgeben
this.gen_o.write_px(text_s);
}
} // ansonsten wie normales Zeichen behandeln
}
pos_i--; // Zeichen erneut verarbeiten
}
break;
case 5:
// post-code-whitespace
switch (source_spl[pos_i]) {
case '\n':
text_s += '\\n';
state_i = 0;
break;
case '\r':
text_s += '\\r';
state_i = 0;
break;
case ' ':
case '\t':
// ggf. sammeln
text_s += source_spl[pos_i];
break;
default:
// Whitespace nach Code erkannt
if (this.preservePostWS_b == true) {
// trotzdem ausgeben
this.gen_o.write_px(text_s);
}
state_i = 0;
pos_i--; // Zeichen erneut verarbeiten
}
break;
}
pos_i++;
}
// welcher Zustand liegt abschließend vor?
if (state_i == 1) {
this.gen_o.write_px(text_s);
} else if (state_i == 2) {
this.gen_o.code_px(code_s);
} else if (state_i == 3) {
this.gen_o.substitute_px(subst_s);
} else if (state_i == 4) {
if (this.preservePreWS_b == true) {
this.gen_o.write_px(text_s);
}
} else if (state_i == 5) {
if (this.preservePostWS_b == true) {
this.gen_o.write_px(text_s);
}
}
return this.gen_o.generate_px();
}
});
// EOF

61
Sammlung/lit-8/js/tm.js Normal file
View File

@ -0,0 +1,61 @@
//------------------------------------------------------------------------------
// Template-Manager
// - Laden und Bereitstellen von Template-Quellen oder anderen Textquellen
//------------------------------------------------------------------------------
// depends-on:
// jquery
// inheritance
//------------------------------------------------------------------------------
// Namensraum TELIB verwenden
TELIB.TemplateManager_cl = Class.create({
initialize: function () {
this.templates_o = {};
this.compiled_o = {};
this.teCompiler_o = new TELIB.TemplateCompiler_cl();
// Templates als Ressource anfordern und speichern
var path_s = "/template/";
$.ajax({
dataType: "json",
url: path_s,
type: 'GET',
context: this
})
.done(function (data_opl) {
this.templates_o = data_opl['templates'];
// Benachrichtigung senden
//+++ Bezeichnung Namensraum korrigieren
LITAPP.es_o.publish_px('app', ['templates.loaded', null]);
})
.fail(function(jqXHR_opl, textStatus_spl) {
alert( "[TELIB.tm] Fehler bei Anforderung: " + textStatus_spl );
});
},
get_px: function (name_spl) {
if (name_spl in this.templates_o) {
return this.templates_o[name_spl];
} else {
return null;
}
},
execute_px: function (name_spl, data_opl) {
var compiled_o = null;
if (name_spl in this.compiled_o) {
compiled_o = this.compiled_o[name_spl];
} else {
// Übersetzen und ausführen
if (name_spl in this.templates_o) {
this.teCompiler_o.reset_px();
compiled_o = this.teCompiler_o.compile_px(this.templates_o[name_spl]);
this.compiled_o[name_spl] = compiled_o;
}
}
if (compiled_o != null) {
return compiled_o(data_opl);
} else {
return null;
}
}
});
// EOF

View File

@ -0,0 +1,19 @@
[global]
tools.log_headers.on: True
tools.sessions.on: False
tools.encode.on: True
tools.encode.encoding:"utf-8"
server.socket_port: 8080
server.socket_timeout:60
server.thread_pool: 10
server.environment: "production"
log.screen: True
[/]
tools.staticdir.root: cherrypy.Application.currentDir_s
tools.staticdir.on = True
tools.staticdir.dir = '.'
tools.staticdir.index = 'html/index.html'

51
Sammlung/lit-8/server.py Normal file
View File

@ -0,0 +1,51 @@
# coding:utf-8
import os.path
import cherrypy
from app import application, template
#----------------------------------------------------------
def main():
#----------------------------------------------------------
# aktuelles Verzeichnis ermitteln, damit es in der Konfigurationsdatei als
# Bezugspunkt verwendet werden kann
try: # aktuelles Verzeichnis als absoluter Pfad
currentDir_s = os.path.dirname(os.path.abspath(__file__))
except:
currentDir_s = os.path.dirname(os.path.abspath(sys.executable))
cherrypy.Application.currentDir_s = currentDir_s
configFileName_s = 'server.conf' # im aktuellen Verzeichnis
if os.path.exists(configFileName_s) == False:
# Datei gibt es nicht
configFileName_s = None
# autoreload und timeout_Monitor hier abschalten
# für cherrypy-Versionen >= "3.1.0" !
cherrypy.engine.autoreload.unsubscribe()
cherrypy.engine.timeout_monitor.unsubscribe()
# 1. Eintrag: Standardverhalten, Berücksichtigung der Konfigurationsangaben im configFile
cherrypy.tree.mount(
None, '/', configFileName_s
)
# 2. Eintrag: Method-Dispatcher für die "Applikation" "lit" vereinbaren
cherrypy.tree.mount(
application.Application_cl(), '/lit', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}}
)
# 3. Eintrag: Method-Dispatcher für die "Applikation" "template" vereinbaren
cherrypy.tree.mount(
template.Template_cl(), '/template', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}}
)
cherrypy.engine.start()
cherrypy.engine.block()
#----------------------------------------------------------
if __name__ == '__main__':
#----------------------------------------------------------
main()

View File

@ -0,0 +1,12 @@
<!-- Template -->
<!-- hier zunächst die einfache Variante -->
@var rows_o = context['data'];@
@for var key_s in rows_o@
<tr id='#key_s#'>
@var row_o = rows_o[key_s];@
<td>Name: #row_o['name']#</td><td>#row_o['typ']#</td><td>#row_o['referenz']#</td>'
</tr>
@endfor@
<!-- EOF -->