]> git.llucax.com Git - z.facultad/75.52/sercom.git/commitdiff
Import inicial.
authorLeandro Lucarella <llucax+sercom@gmail.com>
Fri, 26 Jan 2007 05:45:41 +0000 (05:45 +0000)
committerLeandro Lucarella <llucax+sercom@gmail.com>
Fri, 26 Jan 2007 05:45:41 +0000 (05:45 +0000)
Estructura básica del proyecto con el modelo de datos teóricamente terminado.

38 files changed:
README.txt [new file with mode: 0644]
doc/config-examples/dev.cfg [new file with mode: 0644]
doc/config-examples/sample-prod.cfg [new file with mode: 0644]
doc/config-examples/test.cfg [new file with mode: 0644]
doc/schema.sql [new file with mode: 0644]
doc/testdata.py [new file with mode: 0644]
sercom/__init__.py [new file with mode: 0644]
sercom/config/__init__.py [new file with mode: 0644]
sercom/config/app.cfg [new file with mode: 0644]
sercom/config/log.cfg [new file with mode: 0644]
sercom/controllers.py [new file with mode: 0644]
sercom/json.py [new file with mode: 0644]
sercom/model.py [new file with mode: 0644]
sercom/release.py [new file with mode: 0644]
sercom/static/css/style.css [new file with mode: 0644]
sercom/static/images/favicon.ico [new file with mode: 0644]
sercom/static/images/header_inner.png [new file with mode: 0644]
sercom/static/images/info.png [new file with mode: 0644]
sercom/static/images/ok.png [new file with mode: 0644]
sercom/static/images/tg_under_the_hood.png [new file with mode: 0644]
sercom/static/images/under_the_hood_blue.png [new file with mode: 0644]
sercom/templates/__init__.py [new file with mode: 0644]
sercom/templates/login.kid [new file with mode: 0644]
sercom/templates/master.kid [new file with mode: 0644]
sercom/templates/welcome.kid [new file with mode: 0644]
sercom/tests/__init__.py [new file with mode: 0644]
sercom/tests/test_controllers.py [new file with mode: 0644]
sercom/tests/test_model.py [new file with mode: 0644]
sercom_so.egg-info/PKG-INFO [new file with mode: 0644]
sercom_so.egg-info/SOURCES.txt [new file with mode: 0644]
sercom_so.egg-info/dependency_links.txt [new file with mode: 0644]
sercom_so.egg-info/not-zip-safe [new file with mode: 0644]
sercom_so.egg-info/paster_plugins.txt [new file with mode: 0644]
sercom_so.egg-info/requires.txt [new file with mode: 0644]
sercom_so.egg-info/sqlobject.txt [new file with mode: 0644]
sercom_so.egg-info/top_level.txt [new file with mode: 0644]
setup.py [new file with mode: 0644]
start-sercom.py [new file with mode: 0644]

diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..05be61f
--- /dev/null
@@ -0,0 +1,4 @@
+sercom-so
+
+This is a TurboGears (http://www.turbogears.org) project. It can be
+started by running the start-sercom.py script.
\ No newline at end of file
diff --git a/doc/config-examples/dev.cfg b/doc/config-examples/dev.cfg
new file mode 100644 (file)
index 0000000..9677634
--- /dev/null
@@ -0,0 +1,65 @@
+[global]
+# This is where all of your settings go for your development environment
+# Settings that are the same for both development and production
+# (such as template engine, encodings, etc.) all go in 
+# sercom/config/app.cfg
+
+# DATABASE
+
+# pick the form for your database
+# sqlobject.dburi="postgres://username@hostname/databasename"
+# sqlobject.dburi="mysql://username:password@hostname:port/databasename"
+# sqlobject.dburi="sqlite:///file_name_and_path"
+
+# If you have sqlite, here's a simple default to get you started
+# in development
+sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite"
+
+
+# if you are using a database or table type without transactions
+# (MySQL default, for example), you should turn off transactions
+# by prepending notrans_ on the uri
+# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename"
+
+# for Windows users, sqlite URIs look like:
+# sqlobject.dburi="sqlite:///drive_letter:/path/to/file"
+
+# SERVER
+
+# Some server parameters that you may want to tweak
+# server.socket_port=8080
+
+# Enable the debug output at the end on pages.
+# log_debug_info_filter.on = False
+
+server.environment="development"
+autoreload.package="sercom"
+
+# session_filter.on = True
+
+# Set to True if you'd like to abort execution if a controller gets an
+# unexpected parameter. False by default
+tg.strict_parameters = True
+
+# LOGGING
+# Logging configuration generally follows the style of the standard
+# Python logging module configuration. Note that when specifying
+# log format messages, you need to use *() for formatting variables.
+# Deployment independent log configuration is in sercom/config/log.cfg
+[logging]
+
+[[loggers]]
+[[[sercom]]]
+level='DEBUG'
+qualname='sercom'
+handlers=['debug_out']
+
+[[[allinfo]]]
+level='INFO'
+handlers=['debug_out']
+
+[[[access]]]
+level='INFO'
+qualname='turbogears.access'
+handlers=['access_out']
+propagate=0
diff --git a/doc/config-examples/sample-prod.cfg b/doc/config-examples/sample-prod.cfg
new file mode 100644 (file)
index 0000000..e16e032
--- /dev/null
@@ -0,0 +1,78 @@
+[global]
+# This is where all of your settings go for your production environment.
+# You'll copy this file over to your production server and provide it
+# as a command-line option to your start script.
+# Settings that are the same for both development and production
+# (such as template engine, encodings, etc.) all go in 
+# sercom/config/app.cfg
+
+# DATABASE
+
+# pick the form for your database
+# sqlobject.dburi="postgres://username@hostname/databasename"
+# sqlobject.dburi="mysql://username:password@hostname:port/databasename"
+# sqlobject.dburi="sqlite:///file_name_and_path"
+
+# If you have sqlite, here's a simple default to get you started
+# in development
+sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite"
+
+
+# if you are using a database or table type without transactions
+# (MySQL default, for example), you should turn off transactions
+# by prepending notrans_ on the uri
+# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename"
+
+# for Windows users, sqlite URIs look like:
+# sqlobject.dburi="sqlite:///drive_letter:/path/to/file"
+
+
+# SERVER
+
+server.environment="production"
+
+# Sets the number of threads the server uses
+# server.thread_pool = 1
+
+# if this is part of a larger site, you can set the path
+# to the TurboGears instance here
+# server.webpath=""
+
+# session_filter.on = True
+
+# Set to True if you'd like to abort execution if a controller gets an
+# unexpected parameter. False by default
+# tg.strict_parameters = False
+
+# Set the following to True if you are deploying your app using mod_proxy,
+# mod_rewrite or any other mechanism that forwards requests to your app.
+# base_url_filter.on = False
+# base_url_filter.use_x_forwarded_host = False
+
+# LOGGING
+# Logging configuration generally follows the style of the standard
+# Python logging module configuration. Note that when specifying
+# log format messages, you need to use *() for formatting variables.
+# Deployment independent log configuration is in sercom/config/log.cfg
+[logging]
+
+[[handlers]]
+
+[[[access_out]]]
+# set the filename as the first argument below
+args="('server.log',)"
+class='FileHandler'
+level='INFO'
+formatter='message_only'
+
+[[loggers]]
+[[[sercom]]]
+level='ERROR'
+qualname='sercom'
+handlers=['error_out']
+
+[[[access]]]
+level='INFO'
+qualname='turbogears.access'
+handlers=['access_out']
+propagate=0
diff --git a/doc/config-examples/test.cfg b/doc/config-examples/test.cfg
new file mode 100644 (file)
index 0000000..df909c9
--- /dev/null
@@ -0,0 +1,4 @@
+# You can place test-specific configuration options here (like test db uri, etc)
+
+sqlobject.dburi = "sqlite:///:memory:"
+
diff --git a/doc/schema.sql b/doc/schema.sql
new file mode 100644 (file)
index 0000000..4227483
--- /dev/null
@@ -0,0 +1,218 @@
+
+CREATE TABLE curso (
+    id INTEGER PRIMARY KEY,
+    anio INT NOT NULL,
+    cuatrimestre INT NOT NULL,
+    numero INT NOT NULL,
+    descripcion VARCHAR(255)
+);
+CREATE UNIQUE INDEX curso_pk ON curso (anio, cuatrimestre, numero);
+
+CREATE TABLE usuario (
+    id INTEGER PRIMARY KEY,
+    child_name VARCHAR(255),
+    usuario VARCHAR(10) NOT NULL UNIQUE,
+    contrasenia VARCHAR(255),
+    nombre VARCHAR(255) NOT NULL,
+    email VARCHAR(255),
+    telefono VARCHAR(255),
+    creado TIMESTAMP NOT NULL,
+    observaciones TEXT,
+    activo TINYINT NOT NULL
+);
+
+CREATE TABLE docente (
+    id INTEGER PRIMARY KEY,
+    nombrado TINYINT NOT NULL
+);
+
+CREATE TABLE alumno (
+    id INTEGER PRIMARY KEY,
+    nota DECIMAL(3, 1)
+);
+
+CREATE TABLE tarea (
+    id INTEGER PRIMARY KEY,
+    child_name VARCHAR(255),
+    nombre VARCHAR(30) NOT NULL UNIQUE,
+    descripcion VARCHAR(255)
+);
+
+CREATE TABLE dependencia (
+    padre_id INTEGER NOT NULL CONSTRAINT tarea_id_exists REFERENCES tarea(id),
+    hijo_id INTEGER NOT NULL CONSTRAINT tarea_id_exists REFERENCES tarea(id),
+    orden INT,
+    PRIMARY KEY (padre_id, hijo_id)
+);
+
+CREATE TABLE enunciado (
+    id INTEGER PRIMARY KEY,
+    nombre VARCHAR(60) NOT NULL UNIQUE,
+    descripcion VARCHAR(255),
+    docente_id INT CONSTRAINT docente_id_exists REFERENCES docente(id),
+    creado TIMESTAMP NOT NULL
+);
+
+CREATE TABLE caso_de_prueba (
+    id INTEGER PRIMARY KEY,
+    enunciado_id INT CONSTRAINT enunciado_id_exists REFERENCES enunciado(id),
+    nombre VARCHAR(40) NOT NULL,
+    parametros TEXT NOT NULL,
+    retorno INT,
+    tiempo_cpu FLOAT,
+    descripcion VARCHAR(255)
+);
+CREATE UNIQUE INDEX caso_de_prueba_pk ON caso_de_prueba (enunciado_id, nombre);
+
+CREATE TABLE ejercicio (
+    id INTEGER PRIMARY KEY,
+    curso_id INT NOT NULL CONSTRAINT curso_id_exists REFERENCES curso(id),
+    numero INT NOT NULL,
+    enunciado_id INT NOT NULL CONSTRAINT enunciado_id_exists REFERENCES enunciado(id),
+    grupal TINYINT NOT NULL
+);
+CREATE UNIQUE INDEX ejercicio_pk ON ejercicio (curso_id, numero);
+
+CREATE TABLE instancia_de_entrega (
+    id INTEGER PRIMARY KEY,
+    ejercicio_id INT NOT NULL CONSTRAINT ejercicio_id_exists REFERENCES ejercicio(id),
+    numero INT NOT NULL,
+    inicio TIMESTAMP NOT NULL,
+    fin TIMESTAMP NOT NULL,
+    procesada TINYINT NOT NULL,
+    observaciones TEXT,
+    activo TINYINT NOT NULL
+);
+
+CREATE TABLE instancia_tarea (
+    instancia_id INTEGER NOT NULL CONSTRAINT instancia_id_exists REFERENCES instancia_de_entrega(id),
+    tarea_id INTEGER NOT NULL CONSTRAINT tarea_id_exists REFERENCES tarea(id),
+    orden INT,
+    PRIMARY KEY (instancia_id, tarea_id)
+);
+
+CREATE TABLE docente_inscripto (
+    id INTEGER PRIMARY KEY,
+    curso_id INT NOT NULL CONSTRAINT curso_id_exists REFERENCES curso(id),
+    docente_id INT NOT NULL CONSTRAINT docente_id_exists REFERENCES docente(id),
+    corrige TINYINT NOT NULL,
+    observaciones TEXT
+);
+CREATE UNIQUE INDEX docente_inscripto_pk ON docente_inscripto (curso_id, docente_id);
+
+CREATE TABLE entregador (
+    id INTEGER PRIMARY KEY,
+    child_name VARCHAR(255),
+    nota DECIMAL(3, 1),
+    nota_cursada DECIMAL(3, 1),
+    observaciones TEXT,
+    activo TINYINT NOT NULL
+);
+
+CREATE TABLE grupo (
+    id INTEGER PRIMARY KEY,
+    curso_id INT NOT NULL CONSTRAINT curso_id_exists REFERENCES curso(id),
+    nombre VARCHAR(20) NOT NULL,
+    responsable_id INT CONSTRAINT responsable_id_exists REFERENCES alumno_inscripto(id) 
+);
+
+CREATE TABLE alumno_inscripto (
+    id INTEGER PRIMARY KEY,
+    curso_id INT NOT NULL CONSTRAINT curso_id_exists REFERENCES curso(id),
+    alumno_id INT NOT NULL CONSTRAINT alumno_id_exists REFERENCES alumno(id),
+    condicional TINYINT NOT NULL,
+    tutor_id INT CONSTRAINT tutor_id_exists REFERENCES docente_inscripto(id) 
+);
+CREATE UNIQUE INDEX alumno_inscripto_pk ON alumno_inscripto (curso_id, alumno_id);
+
+CREATE TABLE tutor (
+    id INTEGER PRIMARY KEY,
+    grupo_id INT NOT NULL CONSTRAINT grupo_id_exists REFERENCES grupo(id),
+    docente_id INT NOT NULL CONSTRAINT docente_id_exists REFERENCES docente_inscripto(id),
+    alta TIMESTAMP NOT NULL,
+    baja TIMESTAMP
+);
+CREATE UNIQUE INDEX tutor_pk ON tutor (grupo_id, docente_id);
+
+CREATE TABLE miembro (
+    id INTEGER PRIMARY KEY,
+    grupo_id INT NOT NULL CONSTRAINT grupo_id_exists REFERENCES grupo(id),
+    alumno_id INT NOT NULL CONSTRAINT alumno_id_exists REFERENCES alumno_inscripto(id),
+    nota DECIMAL(3, 1),
+    alta TIMESTAMP NOT NULL,
+    baja TIMESTAMP
+);
+CREATE UNIQUE INDEX miembro_pk ON miembro (grupo_id, alumno_id);
+
+CREATE TABLE entrega (
+    id INTEGER PRIMARY KEY,
+    instancia_id INT NOT NULL CONSTRAINT instancia_id_exists REFERENCES instancia_de_entrega(id),
+    entregador_id INT CONSTRAINT entregador_id_exists REFERENCES entregador(id),
+    fecha TIMESTAMP NOT NULL,
+    correcta TINYINT NOT NULL,
+    observaciones TEXT
+);
+CREATE UNIQUE INDEX entrega_pk ON entrega (instancia_id, entregador_id, fecha);
+
+CREATE TABLE correccion (
+    id INTEGER PRIMARY KEY,
+    instancia_id INT NOT NULL CONSTRAINT instancia_id_exists REFERENCES instancia_de_entrega(id),
+    entregador_id INT NOT NULL CONSTRAINT entregador_id_exists REFERENCES entregador(id),
+    entrega_id INT NOT NULL CONSTRAINT entrega_id_exists REFERENCES entrega(id),
+    corrector_id INT NOT NULL CONSTRAINT corrector_id_exists REFERENCES docente_inscripto(id),
+    asignado TIMESTAMP NOT NULL,
+    corregido TIMESTAMP,
+    nota DECIMAL(3, 1),
+    observaciones TEXT
+);
+CREATE UNIQUE INDEX correccion_pk ON correccion (instancia_id, entregador_id);
+
+CREATE TABLE tarea_ejecutada (
+    id INTEGER PRIMARY KEY,
+    child_name VARCHAR(255),
+    tarea_id INT NOT NULL CONSTRAINT tarea_id_exists REFERENCES tarea(id),
+    entrega_id INT NOT NULL CONSTRAINT entrega_id_exists REFERENCES entrega(id),
+    inicio TIMESTAMP NOT NULL,
+    fin TIMESTAMP,
+    exito INT,
+    observaciones TEXT
+);
+CREATE UNIQUE INDEX tarea_ejecutada_pk ON tarea_ejecutada (tarea_id, entrega_id);
+
+CREATE TABLE prueba (
+    id INTEGER PRIMARY KEY,
+    tarea_ejecutada_id INT NOT NULL CONSTRAINT tarea_ejecutada_id_exists REFERENCES tarea_ejecutada(id),
+    caso_de_prueba_id INT NOT NULL CONSTRAINT caso_de_prueba_id_exists REFERENCES caso_de_prueba(id),
+    inicio TIMESTAMP NOT NULL,
+    fin TIMESTAMP,
+    pasada INT,
+    observaciones TEXT
+);
+CREATE UNIQUE INDEX prueba_pk ON prueba (tarea_ejecutada_id, caso_de_prueba_id);
+
+CREATE TABLE visita (
+    id INTEGER PRIMARY KEY,
+    visit_key VARCHAR(40) NOT NULL UNIQUE,
+    created TIMESTAMP NOT NULL,
+    expiry TIMESTAMP
+);
+
+CREATE TABLE visita_usuario (
+    id INTEGER PRIMARY KEY,
+    visit_key VARCHAR(40) NOT NULL UNIQUE,
+    user_id INT CONSTRAINT usuario_id_exists REFERENCES usuario(id) 
+);
+
+CREATE TABLE rol (
+    id INTEGER PRIMARY KEY,
+    nombre VARCHAR(255) NOT NULL UNIQUE,
+    descripcion VARCHAR(255),
+    creado TIMESTAMP NOT NULL,
+    permisos TEXT NOT NULL
+);
+
+CREATE TABLE rol_usuario (
+    rol_id INT NOT NULL,
+    usuario_id INT NOT NULL
+);
+
diff --git a/doc/testdata.py b/doc/testdata.py
new file mode 100644 (file)
index 0000000..39477e5
--- /dev/null
@@ -0,0 +1,65 @@
+c = Curso(anio=2007, cuatrimestre=1, numero=1, descripcion=u'Martes')
+
+d = Docente(usuario=u'luca', nombre='Leandro Lucarella')
+d.password = 'luca'
+
+a = Alumno(usuario='77891', nombre='Tito Puente')
+a.password = '77891'
+
+r = Rol(nombre='admin', permisos=(entregar_tp, admin))
+d.addRol(r)
+
+r = Rol(nombre='alumno', permisos=(entregar_tp,))
+a.addRol(r)
+
+t1 = Tarea(nombre='compilar')
+t2 = Tarea(nombre='probar')
+t2.dependencias = (t1,)
+t3 = Tarea(nombre=u'configurar detector de copias')
+t4 = Tarea(nombre=u'detectar copias')
+t4.dependencias = (t3, t2)
+
+e1 = Enunciado(nombre=u'Un enunciado', autor=d, descripcion=u'Ejercicio reeee jodido')
+e2 = Enunciado(nombre=u'Otro enunciado', autor=d, descripcion=u'Ejercicio facilongo')
+e3 = Enunciado(nombre=u'Más enunciados', descripcion=u'Ejercicio anónimo')
+
+cp1 = e1.add_caso_de_prueba(u'Sin parámetros', retorno=0, descripcion=u'Un caso')
+cp2 = e1.add_caso_de_prueba(u'2 parámetross', retorno=0, parametros=('--test', '-c'))
+
+ej1 = c.add_ejercicio(1, e1, grupal=True)
+ej2 = c.add_ejercicio(2, e2)
+
+ide = ej1.add_instancia(1, datetime(2007, 1, 25), datetime(2007, 1, 31, 20),
+    observaciones='Entrega fea', activo=False)
+
+di = c.add_docente(d, corrige=True, observaciones=u'Tipo Pulenta')
+
+ai1 = c.add_alumno(a)
+ai2 = c.add_alumno(Alumno(usuario='83525', nombre=u'Pepe Lui'), tutor=di)
+
+g1 = c.add_grupo(5)
+g2 = c.add_grupo(8, responsable=ai2)
+
+g2.add_alumno(ai1)
+g2.add_alumno(ai2)
+g2.add_docente(di)
+
+entrega = ai1.add_entrega(ide)
+ai2.add_entrega(ide, correcta=True)
+entrega2 = g1.add_entrega(ide, correcta=True)
+d.add_entrega(ide, correcta=True, observaciones='Prueba de docente')
+
+te = entrega.add_tarea_ejecutada(t1)
+entrega.add_tarea_ejecutada(t2)
+entrega2.add_tarea_ejecutada(t1, inicio=datetime(2007, 1, 2),
+    fin=datetime.now(), exito=True)
+entrega2.add_tarea_ejecutada(t2, observaciones='Va a tardar')
+
+te.add_prueba(cp1, inicio=datetime(2007, 1, 7), fin=datetime.now(), pasada=True)
+te.add_prueba(cp2)
+
+di.add_correccion(entrega, asignado=datetime(2007, 1, 19), nota=7.5,
+    corregido=datetime.now(), observaciones=u'Le faltó un punto')
+di.add_correccion(entrega2)
+
+__connection__.hub.commit()
diff --git a/sercom/__init__.py b/sercom/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sercom/config/__init__.py b/sercom/config/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sercom/config/app.cfg b/sercom/config/app.cfg
new file mode 100644 (file)
index 0000000..0fd1447
--- /dev/null
@@ -0,0 +1,128 @@
+[global]
+# The settings in this file should not vary depending on the deployment
+# environment. dev.cfg and prod.cfg are the locations for
+# the different deployment settings. Settings in this file will
+# be overridden by settings in those other files.
+
+# The commented out values below are the defaults
+
+# VIEW
+
+# which view (template engine) to use if one is not specified in the
+# template name
+# tg.defaultview = "kid"
+
+# The following kid settings determine the settings used by the kid serializer.
+
+# One of (html|html-strict|xhtml|xhtml-strict|xml|json)
+# kid.outputformat="html"
+
+# kid.encoding="utf-8"
+
+# The sitetemplate is used for overall styling of a site that
+# includes multiple TurboGears applications
+# tg.sitetemplate="<packagename.templates.templatename>"
+
+# Allow every exposed function to be called as json,
+# tg.allow_json = False
+
+# List of Widgets to include on every page.
+# for exemple ['turbogears.mochikit']
+# tg.include_widgets = []
+
+# Set to True if the scheduler should be started
+# tg.scheduler = False
+
+# VISIT TRACKING
+# Each visit to your application will be assigned a unique visit ID tracked via
+# a cookie sent to the visitor's browser.
+# --------------
+
+# Enable Visit tracking
+visit.on = True
+
+# Number of minutes a visit may be idle before it expires.
+# visit.timeout=20
+
+# The name of the cookie to transmit to the visitor's browser.
+# visit.cookie.name="tg-visit"
+
+# Domain name to specify when setting the cookie (must begin with . according to
+# RFC 2109). The default (None) should work for most cases and will default to
+# the machine to which the request was made. NOTE: localhost is NEVER a valid
+# value and will NOT WORK.
+# visit.cookie.domain=None
+
+# Specific path for the cookie
+# visit.cookie.path="/"
+
+# The name of the VisitManager plugin to use for visitor tracking.
+visit.manager = "sqlobject"
+
+# Database class to use for visit tracking
+visit.soprovider.model = "sercom.model.Visita"
+
+# IDENTITY
+# General configuration of the TurboGears Identity management module
+# --------
+
+# Switch to turn on or off the Identity management module
+identity.on = True
+
+# [REQUIRED] URL to which CherryPy will internally redirect when an access
+# control check fails. If Identity management is turned on, a value for this
+# option must be specified.
+identity.failure_url = "/login"
+
+# identity.provider='sqlobject'
+
+# The names of the fields on the login form containing the visitor's user ID
+# and password. In addition, the submit button is specified simply so its
+# existence may be stripped out prior to passing the form data to the target
+# controller.
+identity.form.user_name = "login_user"
+identity.form.password  = "login_password"
+identity.form.submit    = "login_submit"
+
+# What sources should the identity provider consider when determining the
+# identity associated with a request? Comma separated list of identity sources.
+# Valid sources: form, visit, http_auth
+# identity.source="form,http_auth,visit"
+
+# SqlObjectIdentityProvider
+# Configuration options for the default IdentityProvider
+# -------------------------
+
+# The classes you wish to use for your Identity model. Remember to not use reserved
+# SQL keywords for class names (at least unless you specify a different table
+# name using sqlmeta).
+identity.soprovider.model.user = "sercom.model.Usuario"
+identity.soprovider.model.group = "sercom.model.Rol"
+identity.soprovider.model.permission = "sercom.model.Permiso"
+identity.soprovider.model.visit = "sercom.model.VisitaUsuario"
+
+# The password encryption algorithm used when comparing passwords against what's
+# stored in the database. Valid values are 'md5' or 'sha1'. If you do not
+# specify an encryption algorithm, passwords are expected to be clear text.
+# The SqlObjectProvider *will* encrypt passwords supplied as part of your login
+# form.  If you set the password through the password property, like:
+# my_user.password = 'secret'
+# the password will be encrypted in the database, provided identity is up and 
+# running, or you have loaded the configuration specifying what encryption to
+# use (in situations where identity may not yet be running, like tests).
+
+identity.soprovider.encryption_algorithm = 'sha1'
+
+# compress the data sends to the web browser
+# [/] 
+# gzip_filter.on = True
+# gzip_filter.mime_types = ["application/x-javascript", "text/javascript", "text/html", "text/css", "text/plain"]
+
+[/static]
+static_filter.on = True
+static_filter.dir = "%(top_level_dir)s/static"
+
+[/favicon.ico]
+static_filter.on = True
+static_filter.file = "%(top_level_dir)s/static/images/favicon.ico"
+
diff --git a/sercom/config/log.cfg b/sercom/config/log.cfg
new file mode 100644 (file)
index 0000000..ce776f8
--- /dev/null
@@ -0,0 +1,29 @@
+# LOGGING
+# Logging is often deployment specific, but some handlers and
+# formatters can be defined here.
+
+[logging]
+[[formatters]]
+[[[message_only]]]
+format='*(message)s'
+
+[[[full_content]]]
+format='*(asctime)s *(name)s *(levelname)s *(message)s'
+
+[[handlers]]
+[[[debug_out]]]
+class='StreamHandler'
+level='DEBUG'
+args='(sys.stdout,)'
+formatter='full_content'
+
+[[[access_out]]]
+class='StreamHandler'
+level='INFO'
+args='(sys.stdout,)'
+formatter='message_only'
+
+[[[error_out]]]
+class='StreamHandler'
+level='ERROR'
+args='(sys.stdout,)'
diff --git a/sercom/controllers.py b/sercom/controllers.py
new file mode 100644 (file)
index 0000000..4d999ce
--- /dev/null
@@ -0,0 +1,75 @@
+# vim: set et sw=4 sts=4 encoding=utf-8 :
+
+from turbogears import controllers, expose
+from turbogears import widgets as w, validators
+from turbogears import identity, redirect
+from cherrypy import request, response
+from model import *
+# from sercom import json
+
+import logging
+log = logging.getLogger("sercom.controllers")
+
+class Root(controllers.RootController):
+
+    @expose(template='.templates.welcome')
+    @identity.require(identity.has_permission('entregar'))
+    def index(self):
+        import time
+        log.debug('Happy TurboGears Controller Responding For Duty')
+        return dict(now=time.ctime())
+
+    @expose(template='.templates.login')
+    def login(self, forward_url=None, previous_url=None, tg_errors=None, *args,
+            **kw):
+
+        if tg_errors:
+            flash(_(u'Hubo un error en el formulario!'))
+
+        if not identity.current.anonymous \
+                and identity.was_login_attempted() \
+                and not identity.get_identity_errors():
+            raise redirect(forward_url)
+
+        forward_url = None
+        previous_url = request.path
+
+        if identity.was_login_attempted():
+            msg = _(u'Las credenciales proporcionadas no son correctas o no '
+                    'le dan acceso al recurso solicitado.')
+        elif identity.get_identity_errors():
+            msg = _(u'Debe proveer sus credenciales antes de acceder a este '
+                    'recurso.')
+        else:
+            msg = _(u'Por favor ingrese.')
+            forward_url = request.headers.get('Referer', '/')
+
+        fields = [
+            w.TextField(name='login_user', label=_(u'Usuario'),
+                validator=validators.NotEmpty()),
+            w.PasswordField(name='login_password', label=_(u'Contraseña'),
+                validator=validators.NotEmpty())
+        ]
+        if forward_url:
+            fields.append(w.HiddenField(name='forward_url'))
+        fields.extend([w.HiddenField(name=name) for name in request.params
+                if name not in ('login_user', 'login_password', 'login_submit',
+                                'forward_url')])
+
+        submit = w.SubmitButton(name='login_submit')
+
+        login_form = w.TableForm(fields=fields, action=previous_url,
+                        submit_text=_(u'Ingresar'), submit=submit)
+
+        values = dict(forward_url=forward_url)
+        values.update(request.params)
+
+        response.status=403
+        return dict(login_form=login_form, form_data=values, message=msg,
+                logging_in=True)
+
+    @expose()
+    def logout(self):
+        identity.current.logout()
+        raise redirect('/')
+
diff --git a/sercom/json.py b/sercom/json.py
new file mode 100644 (file)
index 0000000..65838e8
--- /dev/null
@@ -0,0 +1,33 @@
+# A JSON-based API(view) for your app.
+# Most rules would look like:
+# @jsonify.when("isinstance(obj, YourClass)")
+# def jsonify_yourclass(obj):
+#     return [obj.val1, obj.val2]
+# @jsonify can convert your objects to following types:
+# lists, dicts, numbers and strings
+
+from turbojson.jsonify import jsonify
+
+from turbojson.jsonify import jsonify_sqlobject
+from sercom.model import User, Group, Permission
+
+@jsonify.when('isinstance(obj, Group)')
+def jsonify_group(obj):
+    result = jsonify_sqlobject( obj )
+    result["users"] = [u.user_name for u in obj.users]
+    result["permissions"] = [p.permission_name for p in obj.permissions]
+    return result
+
+@jsonify.when('isinstance(obj, User)')
+def jsonify_user(obj):
+    result = jsonify_sqlobject( obj )
+    del result['password']
+    result["groups"] = [g.group_name for g in obj.groups]
+    result["permissions"] = [p.permission_name for p in obj.permissions]
+    return result
+
+@jsonify.when('isinstance(obj, Permission)')
+def jsonify_permission(obj):
+    result = jsonify_sqlobject( obj )
+    result["groups"] = [g.group_name for g in obj.groups]
+    return result
diff --git a/sercom/model.py b/sercom/model.py
new file mode 100644 (file)
index 0000000..78772f3
--- /dev/null
@@ -0,0 +1,718 @@
+# vim: set et sw=4 sts=4 encoding=utf-8 :
+
+from datetime import datetime
+from turbogears.database import PackageHub
+from sqlobject import *
+from sqlobject.sqlbuilder import *
+from sqlobject.inheritance import InheritableSQLObject
+from sqlobject.col import PickleValidator
+from turbogears import identity
+
+hub = PackageHub("sercom")
+__connection__ = hub
+
+__all__ = ('Curso', 'Usuario', 'Docente', 'Alumno', 'Tarea', 'CasoDePrueba')
+
+#{{{ Custom Columns
+
+class TupleValidator(PickleValidator):
+    """
+    Validator for tuple types.  A tuple type is simply a pickle type
+    that validates that the represented type is a tuple.
+    """
+
+    def to_python(self, value, state):
+        value = super(TupleValidator, self).to_python(value, state)
+        if value is None:
+            return None
+        if isinstance(value, tuple):
+            return value
+        raise validators.Invalid("expected a tuple in the TupleCol '%s', got %s %r instead" % \
+            (self.name, type(value), value), value, state)
+
+    def from_python(self, value, state):
+        if value is None:
+            return None
+        if not isinstance(value, tuple):
+            raise validators.Invalid("expected a tuple in the TupleCol '%s', got %s %r instead" % \
+                (self.name, type(value), value), value, state)
+        return super(TupleValidator, self).from_python(value, state)
+
+class SOTupleCol(SOPickleCol):
+
+    def __init__(self, **kw):
+        super(SOTupleCol, self).__init__(**kw)
+
+    def createValidators(self):
+        return [TupleValidator(name=self.name)] + \
+            super(SOPickleCol, self).createValidators()
+
+class TupleCol(PickleCol):
+    baseClass = SOTupleCol
+
+#}}}
+
+#{{{ Tablas intermedias
+
+
+# BUG en SQLObject, SQLExpression no tiene cálculo de hash pero se usa como
+# key de un dict. Workarround hasta que lo arreglen.
+SQLExpression.__hash__ = lambda self: hash(str(self))
+
+instancia_tarea_t = table.instancia_tarea
+
+dependencia_t = table.dependencia
+
+#}}}
+
+#{{{ Clases
+
+def srepr(obj): #{{{
+    if obj is not None:
+        return obj.shortrepr()
+    return obj
+#}}}
+
+class ByObject(object): #{{{
+    @classmethod
+    def by(cls, **kw):
+        try:
+            return cls.selectBy(**kw)[0]
+        except IndexError:
+            raise SQLObjectNotFound, "The object %s with columns %s does not exist" % (cls.__name__, kw)
+#}}}
+
+class Curso(SQLObject, ByObject): #{{{
+    # Clave
+    anio            = IntCol(notNone=True)
+    cuatrimestre    = IntCol(notNone=True)
+    numero          = IntCol(notNone=True)
+    pk              = DatabaseIndex(anio, cuatrimestre, numero, unique=True)
+    # Campos
+    descripcion     = UnicodeCol(length=255, default=None)
+    # Joins
+    docentes        = MultipleJoin('DocenteInscripto')
+    alumnos         = MultipleJoin('AlumnoInscripto')
+    grupos          = MultipleJoin('Grupo')
+    ejercicios      = MultipleJoin('Ejercicio', orderBy='numero')
+
+    def add_docente(self, docente, **opts):
+        return DocenteInscripto(cursoID=self.id, docenteID=docente.id, **opts)
+
+    def add_alumno(self, alumno, tutor=None, **opts):
+        tutor_id = tutor and tutor.id
+        return AlumnoInscripto(cursoID=self.id, alumnoID=alumno.id,
+            tutorID=tutor_id, **opts)
+
+    def add_grupo(self, nombre, responsable=None, **opts):
+        resp_id = responsable and responsable.id
+        return Grupo(cursoID=self.id, nombre=unicode(nombre),
+            responsableID=resp_id, **opts)
+
+    def add_ejercicio(self, numero, enunciado, **opts):
+        return Ejercicio(cursoID=self.id, numero=numero,
+            enunciadoID=enunciado.id, **opts)
+
+    def __repr__(self):
+        return 'Curso(id=%s, anio=%s, cuatrimestre=%s, numero=%s, ' \
+            'descripcion=%s)' \
+                % (self.id, self.anio, self.cuatrimestre, self.numero,
+                    self.descripcion)
+
+    def shortrepr(self):
+        return '%s.%s.%s' \
+            % (self.anio, self.cuatrimestre, self.numero, self.descripcion)
+#}}}
+
+class Usuario(InheritableSQLObject, ByObject): #{{{
+    # Clave (para docentes puede ser un nombre de usuario arbitrario)
+    usuario         = UnicodeCol(length=10, alternateID=True)
+    # Campos
+    contrasenia     = UnicodeCol(length=255, default=None)
+    nombre          = UnicodeCol(length=255, notNone=True)
+    email           = UnicodeCol(length=255, default=None)
+    telefono        = UnicodeCol(length=255, default=None)
+    creado          = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    observaciones   = UnicodeCol(default=None)
+    activo          = BoolCol(notNone=True, default=True)
+    # Joins
+    grupos          = RelatedJoin('Grupo')
+    roles           = RelatedJoin('Rol')
+
+    def _get_user_name(self): # para identity
+        return self.usuario
+
+    @classmethod
+    def by_user_name(cls, user_name): # para identity
+        user = cls.byUsuario(user_name)
+        if not user.activo:
+            raise SQLObjectNotFound, "The object %s with user_name %s is " \
+                "not active" % (cls.__name__, user_name)
+        return user
+
+    def _get_groups(self): # para identity
+        return self.roles
+
+    def _get_permissions(self): # para identity
+        perms = set()
+        for g in self.groups:
+            perms.update(g.permisos)
+        return perms
+
+    def _set_password(self, cleartext_password): # para identity
+        self.contrasenia = identity.encrypt_password(cleartext_password)
+
+    def _get_password(self): # para identity
+        return self.contrasenia
+
+    def __repr__(self):
+        raise NotImplementedError, 'Clase abstracta!'
+
+    def shortrepr(self):
+        return '%s (%s)' % (self.usuario, self.nombre)
+#}}}
+
+class Docente(Usuario): #{{{
+    _inheritable = False
+    # Campos
+    nombrado        = BoolCol(notNone=True, default=True)
+    # Joins
+    enunciados      = MultipleJoin('Enunciado')
+    inscripciones   = MultipleJoin('DocenteInscripto')
+
+    def add_entrega(self, instancia, **opts):
+        return Entrega(instanciaID=instancia.id, **opts)
+
+    def add_enunciado(self, nombre, **opts):
+        return Enunciado(autorID=self.id, nombre=nombre, **opts)
+
+    def __repr__(self):
+        return 'Docente(id=%s, usuario=%s, nombre=%s, password=%s, email=%s, ' \
+            'telefono=%s, activo=%s, creado=%s, observaciones=%s)' \
+                % (self.id, self.usuario, self.nombre, self.password,
+                    self.email, self.telefono, self.activo, self.creado,
+                    self.observaciones)
+#}}}
+
+class Alumno(Usuario): #{{{
+    _inheritable = False
+    # Campos
+    nota            = DecimalCol(size=3, precision=1, default=None)
+    # Joins
+    inscripciones   = MultipleJoin('AlumnoInscripto')
+
+    def _get_padron(self): # alias para poder referirse al alumno por padron
+        return self.usuario
+
+    def _set_padron(self, padron):
+        self.usuario = padron
+
+    def __repr__(self):
+        return 'Alumno(id=%s, padron=%s, nombre=%s, password=%s, email=%s, ' \
+            'telefono=%s, activo=%s, creado=%s, observaciones=%s)' \
+                % (self.id, self.padron, self.nombre, self.password, self.email,
+                    self.telefono, self.activo, self.creado, self.observaciones)
+#}}}
+
+class Tarea(InheritableSQLObject, ByObject): #{{{
+    # Clave
+    nombre          = UnicodeCol(length=30, alternateID=True)
+    # Campos
+    descripcion     = UnicodeCol(length=255, default=None)
+    # Joins
+
+    def _get_dependencias(self):
+        OtherTarea = Alias(Tarea, 'other_tarea')
+        self.__dependencias = tuple(Tarea.select(
+            AND(
+                Tarea.q.id == dependencia_t.hijo_id,
+                OtherTarea.q.id == dependencia_t.padre_id,
+                self.id == dependencia_t.padre_id,
+            ),
+            clauseTables=(dependencia_t,),
+            orderBy=dependencia_t.orden,
+        ))
+        return self.__dependencias
+
+    def _set_dependencias(self, dependencias):
+        orden = {}
+        for i, t in enumerate(dependencias):
+            orden[t.id] = i
+        new = frozenset([t.id for t in dependencias])
+        old = frozenset([t.id for t in self.dependencias])
+        dependencias = dict([(t.id, t) for t in dependencias])
+        for tid in old - new: # eliminadas
+            self._connection.query(str(Delete(dependencia_t, where=AND(
+                dependencia_t.padre_id == self.id,
+                dependencia_t.hijo_id == tid))))
+        for tid in new - old: # creadas
+            self._connection.query(str(Insert(dependencia_t, values=dict(
+                padre_id=self.id, hijo_id=tid, orden=orden[tid]
+            ))))
+        for tid in new & old: # actualizados
+            self._connection.query(str(Update(dependencia_t,
+                values=dict(orden=orden[tid]), where=AND(
+                    dependencia_t.padre_id == self.id,
+                    dependencia_t.hijo_id == tid,
+                ))))
+
+    def __repr__(self):
+        return 'Tarea(id=%s, nombre=%s, descripcion=%s)' \
+                % (self.id, self.nombre, self.descripcion)
+
+    def shortrepr(self):
+        return self.nombre
+#}}}
+
+class Enunciado(SQLObject, ByObject): #{{{
+    # Clave
+    nombre          = UnicodeCol(length=60, alternateID=True)
+    # Campos
+    descripcion     = UnicodeCol(length=255, default=None)
+    autor           = ForeignKey('Docente', default=None, dbName='docente_id')
+    creado          = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    # Joins
+    ejercicios      = MultipleJoin('Ejercicio')
+    casos_de_prueba = MultipleJoin('CasoDePrueba')
+
+    def add_caso_de_prueba(self, nombre, **opts):
+        return CasoDePrueba(enunciadoID=self.id, nombre=nombre, **opts)
+
+    def __repr__(self):
+        return 'Enunciado(id=%s, autor=%s, nombre=%s, descripcion=%s, ' \
+            'creado=%s)' \
+                % (self.id, srepr(self.autor), self.nombre, self.descripcion, \
+                    self.creado)
+
+    def shortrepr(self):
+        return self.nombre
+#}}}
+
+class CasoDePrueba(SQLObject): #{{{
+    # Clave
+    enunciado       = ForeignKey('Enunciado')
+    nombre          = UnicodeCol(length=40, notNone=True)
+    pk              = DatabaseIndex(enunciado, nombre, unique=True)
+    # Campos
+#    privado         = IntCol(default=None) TODO iria en instancia_de_entrega_caso_de_prueba
+    parametros      = TupleCol(notNone=True, default=())
+    retorno         = IntCol(default=None)
+    tiempo_cpu      = FloatCol(default=None)
+    descripcion     = UnicodeCol(length=255, default=None)
+    # Joins
+    pruebas         = MultipleJoin('Prueba')
+
+    def __repr__(self):
+        return 'CasoDePrueba(enunciado=%s, nombre=%s, parametros=%s, ' \
+            'retorno=%s, tiempo_cpu=%s, descripcion=%s)' \
+                % (self.enunciado.shortrepr(), self.nombre, self.parametros,
+                    self.retorno, self.tiempo_cpu, self.descripcion)
+
+    def shortrepr(self):
+        return '%s:%s' % (self.enunciado.shortrepr(), self.nombre)
+#}}}
+
+class Ejercicio(SQLObject, ByObject): #{{{
+    # Clave
+    curso           = ForeignKey('Curso', notNone=True)
+    numero          = IntCol(notNone=True)
+    pk              = DatabaseIndex(curso, numero, unique=True)
+    # Campos
+    enunciado       = ForeignKey('Enunciado', notNone=True)
+    grupal          = BoolCol(notNone=True, default=False)
+    # Joins
+    instancias      = MultipleJoin('InstanciaDeEntrega')
+
+    def add_instancia(self, numero, inicio, fin, **opts):
+        return InstanciaDeEntrega(ejercicioID=self.id, numero=numero,
+            inicio=inicio, fin=fin, **opts)
+
+    def __repr__(self):
+        return 'Ejercicio(id=%s, curso=%s, numero=%s, enunciado=%s, ' \
+            'grupal=%s)' \
+                % (self.id, self.curso.shortrepr(), self.numero,
+                    self.enunciado.shortrepr(), self.grupal)
+
+    def shortrepr(self):
+        return '(%s, %s, %s)' \
+            % (self.curso.shortrepr(), self.nombre, \
+                self.enunciado.shortrepr())
+#}}}
+
+class InstanciaDeEntrega(SQLObject, ByObject): #{{{
+    # Clave
+    ejercicio       = ForeignKey('Ejercicio', notNone=True)
+    numero          = IntCol(notNone=True)
+    # Campos
+    inicio          = DateTimeCol(notNone=True)
+    fin             = DateTimeCol(notNone=True)
+    procesada       = BoolCol(notNone=True, default=False)
+    observaciones   = UnicodeCol(default=None)
+    activo          = BoolCol(notNone=True, default=True)
+    # Joins
+    entregas        = MultipleJoin('Entrega', joinColumn='instancia_id')
+    correcciones    = MultipleJoin('Correccion', joinColumn='instancia_id')
+    casos_de_prueba = RelatedJoin('CasoDePrueba') # TODO CasoInstancia -> private
+
+    def _get_tareas(self):
+        self.__tareas = tuple(Tarea.select(
+            AND(
+                Tarea.q.id == instancia_tarea_t.tarea_id,
+                InstanciaDeEntrega.q.id == instancia_tarea_t.instancia_id
+            ),
+            clauseTables=(instancia_tarea_t, InstanciaDeEntrega.sqlmeta.table),
+            orderBy=instancia_tarea_t.orden,
+        ))
+        return self.__tareas
+
+    def _set_tareas(self, tareas):
+        orden = {}
+        for i, t in enumerate(tareas):
+            orden[t.id] = i
+        new = frozenset([t.id for t in tareas])
+        old = frozenset([t.id for t in self.tareas])
+        tareas = dict([(t.id, t) for t in tareas])
+        for tid in old - new: # eliminadas
+            self._connection.query(str(Delete(instancia_tarea_t, where=AND(
+                instancia_tarea_t.instancia_id == self.id,
+                instancia_tarea_t.tarea_id == tid))))
+        for tid in new - old: # creadas
+            self._connection.query(str(Insert(instancia_tarea_t, values=dict(
+                instancia_id=self.id, tarea_id=tid, orden=orden[tid]
+            ))))
+        for tid in new & old: # actualizados
+            self._connection.query(str(Update(instancia_tarea_t,
+                values=dict(orden=orden[tid]), where=AND(
+                    instancia_tarea_t.instancia_id == self.id,
+                    instancia_tarea_t.tarea_id == tid,
+                ))))
+
+    def __repr__(self):
+        return 'InstanciaDeEntrega(id=%s, numero=%s, inicio=%s, fin=%s, ' \
+            'procesada=%s, observaciones=%s, activo=%s)' \
+                % (self.id, self.numero, self.inicio, self.fin,
+                    self.procesada, self.observaciones, self.activo)
+
+    def shortrepr(self):
+        return self.numero
+#}}}
+
+class DocenteInscripto(SQLObject, ByObject): #{{{
+    # Clave
+    curso           = ForeignKey('Curso', notNone=True)
+    docente         = ForeignKey('Docente', notNone=True)
+    pk              = DatabaseIndex(curso, docente, unique=True)
+    # Campos
+    corrige         = BoolCol(notNone=True, default=True)
+    observaciones   = UnicodeCol(default=None)
+    # Joins
+    alumnos         = MultipleJoin('AlumnoInscripto', joinColumn='tutor_id')
+    tutorias        = MultipleJoin('Tutor', joinColumn='docente_id')
+    entregas        = MultipleJoin('Entrega', joinColumn='instancia_id')
+    correcciones    = MultipleJoin('Correccion', joinColumn='corrector_id')
+
+    def add_correccion(self, entrega, **opts):
+        return Correccion(correctorID=self.id, instanciaID=entrega.instancia.id,
+            entregadorID=entrega.entregador.id, entregaID=entrega.id, **opts)
+
+    def __repr__(self):
+        return 'DocenteInscripto(id=%s, docente=%s, corrige=%s, ' \
+            'observaciones=%s' \
+                % (self.id, self.docente.shortrepr(), self.corrige,
+                    self.observaciones)
+
+    def shortrepr(self):
+        return self.docente.shortrepr()
+#}}}
+
+class Entregador(InheritableSQLObject, ByObject): #{{{
+    # Campos
+    nota            = DecimalCol(size=3, precision=1, default=None)
+    nota_cursada    = DecimalCol(size=3, precision=1, default=None)
+    observaciones   = UnicodeCol(default=None)
+    activo          = BoolCol(notNone=True, default=True)
+    # Joins
+    entregas        = MultipleJoin('Entrega')
+    correcciones    = MultipleJoin('Correccion')
+
+    def add_entrega(self, instancia, **opts):
+        return Entrega(entregadorID=self.id, instanciaID=instancia.id, **opts)
+
+    def __repr__(self):
+        raise NotImplementedError, 'Clase abstracta!'
+#}}}
+
+class Grupo(Entregador): #{{{
+    _inheritable = False
+    # Clave
+    curso           = ForeignKey('Curso', notNone=True)
+    nombre          = UnicodeCol(length=20, notNone=True)
+    # Campos
+    responsable     = ForeignKey('AlumnoInscripto', default=None)
+    # Joins
+    miembros        = MultipleJoin('Miembro')
+    tutores         = MultipleJoin('Tutor')
+
+    def add_alumno(self, alumno, **opts):
+        return Miembro(grupoID=self.id, alumnoID=alumno.id, **opts)
+
+    def add_docente(self, docente, **opts):
+        return Tutor(grupoID=self.id, docenteID=docente.id, **opts)
+
+    def __repr__(self):
+        return 'Grupo(id=%s, nombre=%s, responsable=%s, nota=%s, ' \
+            'nota_cursada=%s, observaciones=%s, activo=%s)' \
+                % (self.id, self.nombre, srepr(self.responsable), self.nota,
+                    self.nota_cursada, self.observaciones, self.activo)
+
+    def shortrepr(self):
+        return 'grupo:' + self.nombre
+#}}}
+
+class AlumnoInscripto(Entregador): #{{{
+    _inheritable = False
+    # Clave
+    curso               = ForeignKey('Curso', notNone=True)
+    alumno              = ForeignKey('Alumno', notNone=True)
+    pk                  = DatabaseIndex(curso, alumno, unique=True)
+    # Campos
+    condicional         = BoolCol(notNone=True, default=False)
+    tutor               = ForeignKey('DocenteInscripto', default=None)
+    # Joins
+    responsabilidades   = MultipleJoin('Grupo', joinColumn='responsable_id')
+    membresias          = MultipleJoin('Miembro', joinColumn='alumno_id')
+    entregas            = MultipleJoin('Entrega', joinColumn='alumno_id')
+    correcciones        = MultipleJoin('Correccion', joinColumn='alumno_id')
+
+    def __repr__(self):
+        return 'AlumnoInscripto(id=%s, alumno=%s, condicional=%s, nota=%s, ' \
+            'nota_cursada=%s, tutor=%s, observaciones=%s, activo=%s)' \
+                % (self.id, self.alumno.shortrepr(), self.condicional,
+                    self.nota, self.nota_cursada, srepr(self.tutor),
+                    self.observaciones, self.activo)
+
+    def shortrepr(self):
+        return self.alumno.shortrepr()
+#}}}
+
+class Tutor(SQLObject, ByObject): #{{{
+    # Clave
+    grupo           = ForeignKey('Grupo', notNone=True)
+    docente         = ForeignKey('DocenteInscripto', notNone=True)
+    pk              = DatabaseIndex(grupo, docente, unique=True)
+    # Campos
+    alta            = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    baja            = DateTimeCol(default=None)
+
+    def __repr__(self):
+        return 'Tutor(docente=%s, grupo=%s, alta=%s, baja=%s)' \
+                % (self.docente.shortrepr(), self.grupo.shortrepr(),
+                    self.alta, self.baja)
+
+    def shortrepr(self):
+        return '%s-%s' % (self.docente.shortrepr(), self.grupo.shortrepr())
+#}}}
+
+class Miembro(SQLObject, ByObject): #{{{
+    # Clave
+    grupo           = ForeignKey('Grupo', notNone=True)
+    alumno          = ForeignKey('AlumnoInscripto', notNone=True)
+    pk              = DatabaseIndex(grupo, alumno, unique=True)
+    # Campos
+    nota            = DecimalCol(size=3, precision=1, default=None)
+    alta            = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    baja            = DateTimeCol(default=None)
+
+    def __repr__(self):
+        return 'Miembro(alumno=%s, grupo=%s, nota=%s, alta=%s, baja=%s)' \
+                % (self.alumno.shortrepr(), self.grupo.shortrepr(),
+                    self.nota, self.alta, self.baja)
+
+    def shortrepr(self):
+        return '%s-%s' % (self.alumno.shortrepr(), self.grupo.shortrepr())
+#}}}
+
+class Entrega(SQLObject, ByObject): #{{{
+    # Clave
+    instancia       = ForeignKey('InstanciaDeEntrega', notNone=True)
+    entregador      = ForeignKey('Entregador', default=None) # Si es None era un Docente
+    fecha           = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    pk              = DatabaseIndex(instancia, entregador, fecha, unique=True)
+    # Campos
+    correcta        = BoolCol(notNone=True, default=False)
+    observaciones   = UnicodeCol(default=None)
+    # Joins
+    tareas          = MultipleJoin('TareaEjecutada')
+    # Para generar código
+    codigo_dict     = r'0123456789abcdefghijklmnopqrstuvwxyz_.,*@#+'
+    codigo_format   = r'%m%d%H%M%S'
+
+    def add_tarea_ejecutada(self, tarea, **opts):
+        return TareaEjecutada(entregaID=self.id, tareaID=tarea.id, **opts)
+
+    def _get_codigo(self):
+        if not hasattr(self, '_codigo'): # cache
+            n = long(self.fecha.strftime(Entrega.codigo_format))
+            d = Entrega.codigo_dict
+            l = len(d)
+            res = ''
+            while n:
+                    res += d[n % l]
+                    n /= l
+            self._codigo = res
+        return self._codigo
+
+    def _set_fecha(self, fecha):
+        self._SO_set_fecha(fecha)
+        if hasattr(self, '_codigo'): del self._codigo # bye, bye cache!
+
+    def __repr__(self):
+        return 'Entrega(instancia=%s, entregador=%s, codigo=%s, fecha=%s, ' \
+            'correcta=%s, observaciones=%s)' \
+                % (self.instancia.shortrepr(), srepr(self.entregador),
+                    self.codigo, self.fecha, self.correcta, self.observaciones)
+
+    def shortrepr(self):
+        return '%s-%s-%s' % (self.instancia.shortrepr(), srepr(self.entregador),
+            self.codigo)
+#}}}
+
+class Correccion(SQLObject, ByObject): #{{{
+    # Clave
+    instancia       = ForeignKey('InstanciaDeEntrega', notNone=True)
+    entregador      = ForeignKey('Entregador', notNone=True) # Docente no tiene
+    pk              = DatabaseIndex(instancia, entregador, unique=True)
+    # Campos
+    entrega         = ForeignKey('Entrega', notNone=True)
+    corrector       = ForeignKey('DocenteInscripto', notNone=True)
+    asignado        = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    corregido       = DateTimeCol(default=None)
+    nota            = DecimalCol(size=3, precision=1, default=None)
+    observaciones   = UnicodeCol(default=None)
+
+    def __repr__(self):
+        return 'Correccion(instancia=%s, entregador=%s, entrega=%s, ' \
+            'corrector=%s, asignado=%s, corregido=%s, nota=%s, ' \
+            'observaciones=%s)' \
+                % (self.instancia.shortrepr(), self.entregador.shortrepr(),
+                    self.entrega.shortrepr(), self.corrector, self.asignado,
+                    self.corregido, self.nota, self.observaciones)
+
+    def shortrepr(self):
+        return '%s,%s' % (self.entrega.shortrepr(), self.corrector.shortrepr())
+#}}}
+
+class TareaEjecutada(InheritableSQLObject, ByObject): #{{{
+    # Clave
+    tarea           = ForeignKey('Tarea', notNone=True)
+    entrega         = ForeignKey('Entrega', notNone=True)
+    pk              = DatabaseIndex(tarea, entrega, unique=True)
+    # Campos
+    inicio          = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    fin             = DateTimeCol(default=None)
+    exito           = IntCol(default=None)
+    observaciones   = UnicodeCol(default=None)
+    # Joins
+    pruebas         = MultipleJoin('Prueba')
+
+    def add_prueba(self, caso_de_prueba, **opts):
+        return Prueba(tarea_ejecutadaID=self.id,
+            caso_de_pruebaID=caso_de_prueba.id, **opts)
+
+    def __repr__(self):
+        return 'TareaEjecutada(tarea=%s, entrega=%s, inicio=%s, fin=%s, ' \
+            'exito=%s, observaciones=%s)' \
+                % (self.tarea.shortrepr(), self.entrega.shortrepr(),
+                    self.inicio, self.fin, self.exito, self.observaciones)
+
+    def shortrepr(self):
+        return '%s-%s' % (self.tarea.shortrepr(), self.entrega.shortrepr())
+#}}}
+
+class Prueba(SQLObject): #{{{
+    # Clave
+    tarea_ejecutada = ForeignKey('TareaEjecutada', notNone=True)
+    caso_de_prueba  = ForeignKey('CasoDePrueba', notNone=True)
+    pk              = DatabaseIndex(tarea_ejecutada, caso_de_prueba, unique=True)
+    # Campos
+    inicio          = DateTimeCol(notNone=True, default=DateTimeCol.now)
+    fin             = DateTimeCol(default=None)
+    pasada          = IntCol(default=None)
+    observaciones   = UnicodeCol(default=None)
+
+    def __repr__(self):
+        return 'Prueba(tarea_ejecutada=%s, caso_de_prueba=%s, inicio=%s, ' \
+            'fin=%s, pasada=%s, observaciones=%s)' \
+                % (self.tarea_ejecutada.shortrepr(),
+                    self.caso_de_prueba.shortrepr(), self.inicio, self.fin,
+                    self.pasada, self.observaciones)
+
+    def shortrepr(self):
+        return '%s:%s' % (self.tarea_ejecutada.shortrepr(),
+            self.caso_de_prueba.shortrerp())
+#}}}
+
+#{{{ Específico de Identity
+
+class Visita(SQLObject): #{{{
+    visit_key   = StringCol(length=40, alternateID=True,
+                    alternateMethodName="by_visit_key")
+    created     = DateTimeCol(notNone=True, default=datetime.now)
+    expiry      = DateTimeCol()
+
+    @classmethod
+    def lookup_visit(cls, visit_key):
+        try:
+            return cls.by_visit_key(visit_key)
+        except SQLObjectNotFound:
+            return None
+#}}}
+
+class VisitaUsuario(SQLObject): #{{{
+    # Clave
+    visit_key   = StringCol(length=40, alternateID=True,
+                          alternateMethodName="by_visit_key")
+    # Campos
+    user_id     = IntCol() # Negrada de identity
+#}}}
+
+
+class Rol(SQLObject): #{{{
+    # Clave
+    nombre      = UnicodeCol(length=255, alternateID=True,
+                    alternateMethodName="by_group_name")
+    # Campos
+    descripcion = UnicodeCol(length=255, default=None)
+    creado      = DateTimeCol(notNone=True, default=datetime.now)
+    permisos    = TupleCol(notNone=True)
+    # Joins
+    usuarios    = RelatedJoin('Usuario')
+#}}}
+
+# No es un SQLObject porque no tiene sentido agregar/sacar permisos, están
+# hardcodeados en el código
+class Permiso(object): #{{{
+    def __init__(self, nombre, descripcion):
+        self.nombre = nombre
+        self.descripcion = descripcion
+
+    @classmethod
+    def createTable(cls, ifNotExists): # para identity
+        pass
+
+    @property
+    def permission_name(self): # para identity
+        return self.nombre
+
+    def __repr__(self):
+        return self.nombre
+#}}}
+
+# TODO ejemplos
+entregar_tp = Permiso(u'entregar', u'Permite entregar trabajos prácticos')
+admin = Permiso(u'admin', u'Permite hacer ABMs arbitrarios')
+
+#}}} Identity
+
+#}}} Clases
+
diff --git a/sercom/release.py b/sercom/release.py
new file mode 100644 (file)
index 0000000..f0e8327
--- /dev/null
@@ -0,0 +1,14 @@
+# Release information about sercom-so
+
+version = "1.0"
+
+# description = "Your plan to rule the world"
+# long_description = "More description about your plan"
+# author = "Your Name Here"
+# email = "YourEmail@YourDomain"
+# copyright = "Vintage 2006 - a good year indeed"
+
+# if it's open source, you might want to specify these
+# url = "http://yourcool.site/"
+# download_url = "http://yourcool.site/download"
+# license = "MIT"
diff --git a/sercom/static/css/style.css b/sercom/static/css/style.css
new file mode 100644 (file)
index 0000000..1bc7d64
--- /dev/null
@@ -0,0 +1,124 @@
+/*\r
+ * Quick mash-up of CSS for the TG quick start page.\r
+ */\r
+\r
+html, body, th, td {\r
+  color: black;\r
+  background-color: #ddd;\r
+  font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif;\r
+  margin: 0;\r
+  padding: 0;\r
+}\r
+\r
+#header {\r
+  height: 80px;\r
+  width: 777px;\r
+  background: blue URL('../images/header_inner.png') no-repeat;\r
+  border-left: 1px solid #aaa;\r
+  border-right: 1px solid #aaa;\r
+  margin: 0 auto 0 auto;\r
+}\r
+\r
+a.link, a, a.active {\r
+  color: #369;\r
+}\r
+\r
+\r
+#main_content {\r
+  color: black;\r
+  font-size: 127%;\r
+  background-color: white;\r
+  width: 757px;\r
+  margin: 0 auto 0 auto;\r
+  border-left: 1px solid #aaa;\r
+  border-right: 1px solid #aaa;\r
+  padding: 10px;\r
+}\r
+\r
+#sidebar {\r
+  border: 1px solid #aaa;\r
+  background-color: #eee;\r
+  margin: 0.5em;\r
+  padding: 1em;\r
+  float: right;\r
+  width: 200px;\r
+  font-size: 88%;\r
+}\r
+\r
+#sidebar h2 {\r
+  margin-top: 0;\r
+}\r
+\r
+#sidebar ul {\r
+  margin-left: 1.5em;\r
+  padding-left: 0;\r
+}\r
+\r
+h1,h2,h3,h4,h5,h6,#getting_started_steps {\r
+  font-family: "Century Schoolbook L", Georgia, serif;\r
+  font-weight: bold;\r
+}\r
+\r
+h2 {\r
+  font-size: 150%;\r
+}\r
+\r
+#getting_started_steps a {\r
+  text-decoration: none;\r
+}\r
+\r
+#getting_started_steps a:hover {\r
+  text-decoration: underline;\r
+}\r
+\r
+#getting_started_steps li {\r
+  font-size: 80%;\r
+  margin-bottom: 0.5em;\r
+}\r
+\r
+#getting_started_steps h2 {\r
+  font-size: 120%;\r
+}\r
+\r
+#getting_started_steps p {\r
+  font: 100% "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif;\r
+}\r
+\r
+#footer {\r
+  border: 1px solid #aaa;\r
+  border-top: 0px none;\r
+  color: #999;\r
+  background-color: white;\r
+  padding: 10px;\r
+  font-size: 80%;\r
+  text-align: center;\r
+  width: 757px;\r
+  margin: 0 auto 1em auto;\r
+}\r
+\r
+.code {\r
+  font-family: monospace;\r
+}\r
+\r
+span.code {\r
+  font-weight: bold;\r
+  background: #eee;\r
+}\r
+\r
+#status_block {\r
+  margin: 0 auto 0.5em auto;\r
+  padding: 15px 10px 15px 55px;\r
+  background: #cec URL('../images/ok.png') left center no-repeat;\r
+  border: 1px solid #9c9;\r
+  width: 450px;\r
+  font-size: 120%;\r
+  font-weight: bolder;\r
+}\r
+\r
+.notice {\r
+  margin: 0.5em auto 0.5em auto;\r
+  padding: 15px 10px 15px 55px;\r
+  width: 450px;\r
+  background: #eef URL('../images/info.png') left center no-repeat;\r
+  border: 1px solid #cce;\r
+}\r
diff --git a/sercom/static/images/favicon.ico b/sercom/static/images/favicon.ico
new file mode 100644 (file)
index 0000000..332557b
Binary files /dev/null and b/sercom/static/images/favicon.ico differ
diff --git a/sercom/static/images/header_inner.png b/sercom/static/images/header_inner.png
new file mode 100644 (file)
index 0000000..2b2d87d
Binary files /dev/null and b/sercom/static/images/header_inner.png differ
diff --git a/sercom/static/images/info.png b/sercom/static/images/info.png
new file mode 100644 (file)
index 0000000..329c523
Binary files /dev/null and b/sercom/static/images/info.png differ
diff --git a/sercom/static/images/ok.png b/sercom/static/images/ok.png
new file mode 100644 (file)
index 0000000..fee6751
Binary files /dev/null and b/sercom/static/images/ok.png differ
diff --git a/sercom/static/images/tg_under_the_hood.png b/sercom/static/images/tg_under_the_hood.png
new file mode 100644 (file)
index 0000000..bc9c79c
Binary files /dev/null and b/sercom/static/images/tg_under_the_hood.png differ
diff --git a/sercom/static/images/under_the_hood_blue.png b/sercom/static/images/under_the_hood_blue.png
new file mode 100644 (file)
index 0000000..90e84b7
Binary files /dev/null and b/sercom/static/images/under_the_hood_blue.png differ
diff --git a/sercom/templates/__init__.py b/sercom/templates/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sercom/templates/login.kid b/sercom/templates/login.kid
new file mode 100644 (file)
index 0000000..5d7a239
--- /dev/null
@@ -0,0 +1,82 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+    xmlns:py="http://purl.org/kid/ns#">
+
+<head>
+    <meta content="text/html; charset=UTF-8"
+        http-equiv="content-type" py:replace="''"/>
+    <title>Login</title>
+    <style type="text/css">
+        #loginBox
+        {
+            width: 30%;
+            margin: auto;
+            margin-top: 10%;
+            padding-left: 10%;
+            padding-right: 10%;
+            padding-top: 5%;
+            padding-bottom: 5%;
+            font-family: verdana;
+            font-size: 10px;
+            background-color: #eee;
+            border: 2px solid #ccc;
+        }
+
+        #loginBox h1
+        {
+            font-size: 42px;
+            font-family: "Trebuchet MS";
+            margin: 0;
+            color: #ddd;
+        }
+
+        #loginBox p
+        {
+            position: relative;
+            top: -1.5em;
+            padding-left: 4em;
+            font-size: 12px;
+            margin: 0;
+            color: #666;
+        }
+
+        #loginBox table
+        {
+            table-layout: fixed;
+            border-spacing: 0;
+            width: 100%;
+        }
+
+        #loginBox td.label
+        {
+            width: 33%;
+            text-align: right;
+        }
+
+        #loginBox td.field
+        {
+            width: 66%;
+        }
+
+        #loginBox td.field input
+        {
+            width: 100%;
+        }
+
+        #loginBox td.buttons
+        {
+            text-align: right;
+        }
+
+    </style>
+</head>
+
+<body>
+    <div id="loginBox">
+        <h1>Identificación</h1>
+        <p py:content="message">Mensaje de error</p>
+       <p py:content="login_form(value=form_data)">Formulario de login</p>
+    </div>
+</body>
+</html>
diff --git a/sercom/templates/master.kid b/sercom/templates/master.kid
new file mode 100644 (file)
index 0000000..7a8e03c
--- /dev/null
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<?python import sitetemplate ?>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" py:extends="sitetemplate">
+
+<head py:match="item.tag=='{http://www.w3.org/1999/xhtml}head'" py:attrs="item.items()">
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+    <title py:replace="''">Sercom</title>
+    <meta py:replace="item[:]"/>
+    <style type="text/css">
+        #pageLogin
+        {
+            font-size: 10px;
+            font-family: verdana;
+            text-align: right;
+        }
+    </style>
+    <style type="text/css" media="screen">
+@import "/static/css/style.css";
+</style>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+    <div py:if="tg.config('identity.on',False) and not 'logging_in' in locals()"
+        id="pageLogin">
+        <span py:if="tg.identity.anonymous">
+            <a href="/login">Login</a>
+        </span>
+        <span py:if="not tg.identity.anonymous">
+            Bienvenido ${tg.identity.user.nombre}.
+            <a href="/logout">Logout</a>
+        </span>
+    </div>
+    <div id="header">&nbsp;</div>
+    <div id="main_content">
+    <div py:if="tg_flash" class="flash" py:content="tg_flash"></div>
+
+    <div py:replace="[item.text]+item[:]"/>
+
+       <!-- End of main_content -->
+       </div>
+<div id="footer"> <img src="/static/images/under_the_hood_blue.png" alt="TurboGears under the hood" />
+  <p>TurboGears is a open source front-to-back web development
+    framework written in Python</p>
+  <p>Copyright &copy; 2006 Kevin Dangoor</p>
+</div>
+</body>
+
+</html>
diff --git a/sercom/templates/welcome.kid b/sercom/templates/welcome.kid
new file mode 100644 (file)
index 0000000..d23eb9f
--- /dev/null
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+    py:extends="'master.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Welcome to TurboGears</title>
+</head>
+<body>
+
+  <div id="status_block">Your application is now running</div>
+  <div id="sidebar">
+    <h2>Learn more</h2>
+    Learn more about TurboGears and take part in its
+    development
+    <ul class="links">
+      <li><a href="http://www.turbogears.org">Official website</a></li>
+      <li><a href="http://docs.turbogears.org">Documentation</a></li>
+      <li><a href="http://trac.turbogears.org/turbogears/">Trac
+        (bugs/suggestions)</a></li>
+      <li><a href="http://groups.google.com/group/turbogears"> Mailing list</a> </li>
+    </ul>
+    <span py:replace="now">now</span>
+  </div>
+  <div id="getting_started">
+    <ol id="getting_started_steps">
+      <li class="getting_started">
+        <h3>Model</h3>
+        <p> <a href="http://docs.turbogears.org/1.0/GettingStarted/DefineDatabase">Design models</a> in the <span class="code">model.py</span>.<br/>
+          Edit <span class="code">dev.cfg</span> to <a href="http://docs.turbogears.org/1.0/GettingStarted/UseDatabase">use a different backend</a>, or start with a pre-configured SQLite database. <br/>
+          Use script <span class="code">tg-admin sql create</span> to create the database tables.</p>
+      </li>
+      <li class="getting_started">
+        <h3>View</h3>
+        <p> Edit <a href="http://docs.turbogears.org/1.0/GettingStarted/Kid">html-like templates</a> in the <span class="code">/templates</span> folder;<br/>
+        Put all <a href="http://docs.turbogears.org/1.0/StaticFiles">static contents</a> in the <span class="code">/static</span> folder. </p>
+      </li>
+      <li class="getting_started">
+        <h3>Controller</h3>
+        <p> Edit <span class="code"> controllers.py</span> and <a href="http://docs.turbogears.org/1.0/GettingStarted/CherryPy">build your
+          website structure</a> with the simplicity of Python objects. <br/>
+          TurboGears will automatically reload itself when you modify your project. </p>
+      </li>
+    </ol>
+    <div class="notice"> If you create something cool, please <a href="http://groups.google.com/group/turbogears">let people know</a>, and consider contributing something back to the <a href="http://groups.google.com/group/turbogears">community</a>.</div>
+  </div>
+  <!-- End of getting_started -->
+</body>
+</html>
diff --git a/sercom/tests/__init__.py b/sercom/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sercom/tests/test_controllers.py b/sercom/tests/test_controllers.py
new file mode 100644 (file)
index 0000000..5628e49
--- /dev/null
@@ -0,0 +1,33 @@
+import turbogears
+from nose import with_setup
+from turbogears import testutil
+from sercom.controllers import Root
+import cherrypy
+
+def teardown_func():
+    """Tests for apps using identity need to stop CP/TG after each test to
+    stop the VisitManager thread. See http://trac.turbogears.org/turbogears/ticket/1217
+    for details.
+    """
+    turbogears.startup.stopTurboGears()
+
+cherrypy.root = Root()
+
+def test_method():
+    "the index method should return a string called now"
+    import types
+    result = testutil.call(cherrypy.root.index)
+    assert type(result["now"]) == types.StringType
+test_method = with_setup(teardown=teardown_func)(test_method)
+
+def test_indextitle():
+    "The indexpage should have the right title"
+    testutil.createRequest("/")
+    assert "<TITLE>Welcome to TurboGears</TITLE>" in cherrypy.response.body[0]
+test_indextitle = with_setup(teardown=teardown_func)(test_indextitle)
+
+def test_logintitle():
+    "login page should have the right title"
+    testutil.createRequest("/login")
+    assert "<TITLE>Login</TITLE>" in cherrypy.response.body[0]
+test_logintitle = with_setup(teardown=teardown_func)(test_logintitle)
diff --git a/sercom/tests/test_model.py b/sercom/tests/test_model.py
new file mode 100644 (file)
index 0000000..2b96ca0
--- /dev/null
@@ -0,0 +1,22 @@
+# If your project uses a database, you can set up database tests
+# similar to what you see below. Be sure to set the db_uri to
+# an appropriate uri for your testing database. sqlite is a good
+# choice for testing, because you can use an in-memory database
+# which is very fast.
+
+from turbogears import testutil, database
+# from sercom.model import YourDataClass, User
+
+# database.set_db_uri("sqlite:///:memory:")
+
+# class TestUser(testutil.DBTest):
+#     def get_model(self):
+#         return User
+#     def test_creation(self):
+#         "Object creation should set the name"
+#         obj = User(user_name = "creosote",
+#                       email_address = "spam@python.not",
+#                       display_name = "Mr Creosote",
+#                       password = "Wafer-thin Mint")
+#         assert obj.display_name == "Mr Creosote"
+
diff --git a/sercom_so.egg-info/PKG-INFO b/sercom_so.egg-info/PKG-INFO
new file mode 100644 (file)
index 0000000..07480d9
--- /dev/null
@@ -0,0 +1,15 @@
+Metadata-Version: 1.0
+Name: sercom-so
+Version: 1.0
+Summary: UNKNOWN
+Home-page: UNKNOWN
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Framework :: TurboGears
diff --git a/sercom_so.egg-info/SOURCES.txt b/sercom_so.egg-info/SOURCES.txt
new file mode 100644 (file)
index 0000000..7d9e209
--- /dev/null
@@ -0,0 +1,21 @@
+README.txt
+setup.py
+start-sercom.py
+sercom/__init__.py
+sercom/controllers.py
+sercom/json.py
+sercom/model.py
+sercom/release.py
+sercom/config/__init__.py
+sercom/templates/__init__.py
+sercom/tests/__init__.py
+sercom/tests/test_controllers.py
+sercom/tests/test_model.py
+sercom_so.egg-info/PKG-INFO
+sercom_so.egg-info/SOURCES.txt
+sercom_so.egg-info/dependency_links.txt
+sercom_so.egg-info/not-zip-safe
+sercom_so.egg-info/paster_plugins.txt
+sercom_so.egg-info/requires.txt
+sercom_so.egg-info/sqlobject.txt
+sercom_so.egg-info/top_level.txt
diff --git a/sercom_so.egg-info/dependency_links.txt b/sercom_so.egg-info/dependency_links.txt
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/sercom_so.egg-info/not-zip-safe b/sercom_so.egg-info/not-zip-safe
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/sercom_so.egg-info/paster_plugins.txt b/sercom_so.egg-info/paster_plugins.txt
new file mode 100644 (file)
index 0000000..14fec70
--- /dev/null
@@ -0,0 +1,2 @@
+TurboGears
+PasteScript
diff --git a/sercom_so.egg-info/requires.txt b/sercom_so.egg-info/requires.txt
new file mode 100644 (file)
index 0000000..f06697f
--- /dev/null
@@ -0,0 +1 @@
+TurboGears >= 1.0
\ No newline at end of file
diff --git a/sercom_so.egg-info/sqlobject.txt b/sercom_so.egg-info/sqlobject.txt
new file mode 100644 (file)
index 0000000..04fa0c4
--- /dev/null
@@ -0,0 +1,2 @@
+db_module=sercom.model
+history_dir=$base/sercom/sqlobject-history
diff --git a/sercom_so.egg-info/top_level.txt b/sercom_so.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..99331ce
--- /dev/null
@@ -0,0 +1 @@
+sercom
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..ee061f9
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,62 @@
+from setuptools import setup, find_packages
+from turbogears.finddata import find_package_data
+
+import os
+execfile(os.path.join("sercom", "release.py"))
+
+setup(
+    name="sercom-so",
+    version=version,
+    
+    # uncomment the following lines if you fill them out in release.py
+    #description=description,
+    #author=author,
+    #author_email=email,
+    #url=url,
+    #download_url=download_url,
+    #license=license,
+    
+    install_requires = [
+        "TurboGears >= 1.0",
+    ],
+    scripts = ["start-sercom.py"],
+    zip_safe=False,
+    packages=find_packages(),
+    package_data = find_package_data(where='sercom',
+                                     package='sercom'),
+    keywords = [
+        # Use keywords if you'll be adding your package to the
+        # Python Cheeseshop
+        
+        # if this has widgets, uncomment the next line
+        # 'turbogears.widgets',
+        
+        # if this has a tg-admin command, uncomment the next line
+        # 'turbogears.command',
+        
+        # if this has identity providers, uncomment the next line
+        # 'turbogears.identity.provider',
+    
+        # If this is a template plugin, uncomment the next line
+        # 'python.templating.engines',
+        
+        # If this is a full application, uncomment the next line
+        # 'turbogears.app',
+    ],
+    classifiers = [
+        'Development Status :: 3 - Alpha',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Framework :: TurboGears',
+        # if this is an application that you'll distribute through
+        # the Cheeseshop, uncomment the next line
+        # 'Framework :: TurboGears :: Applications',
+        
+        # if this is a package that includes widgets that you'll distribute
+        # through the Cheeseshop, uncomment the next line
+        # 'Framework :: TurboGears :: Widgets',
+    ],
+    test_suite = 'nose.collector',
+    )
+    
diff --git a/start-sercom.py b/start-sercom.py
new file mode 100644 (file)
index 0000000..b32eb45
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+import pkg_resources
+pkg_resources.require("TurboGears")
+
+from turbogears import update_config, start_server
+import cherrypy
+cherrypy.lowercase_api = True
+from os.path import *
+import sys
+
+# first look on the command line for a desired config file,
+# if it's not on the command line, then
+# look for setup.py in this directory. If it's not there, this script is
+# probably installed
+if len(sys.argv) > 1:
+    update_config(configfile=sys.argv[1], 
+        modulename="sercom.config")
+elif exists(join(dirname(__file__), "setup.py")):
+    update_config(configfile="dev.cfg",modulename="sercom.config")
+else:
+    update_config(configfile="prod.cfg",modulename="sercom.config")
+
+from sercom.controllers import Root
+
+start_server(Root())