From: Leandro Lucarella Date: Fri, 26 Jan 2007 05:45:41 +0000 (+0000) Subject: Import inicial. X-Git-Tag: pre-merge~170 X-Git-Url: https://git.llucax.com/z.facultad/75.52/sercom.git/commitdiff_plain/2344dcf6736372c1dd359a95819fb3c2e131afc2?ds=sidebyside Import inicial. Estructura básica del proyecto con el modelo de datos teóricamente terminado. --- 2344dcf6736372c1dd359a95819fb3c2e131afc2 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..05be61f --- /dev/null +++ b/README.txt @@ -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 index 0000000..9677634 --- /dev/null +++ b/doc/config-examples/dev.cfg @@ -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 index 0000000..e16e032 --- /dev/null +++ b/doc/config-examples/sample-prod.cfg @@ -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 index 0000000..df909c9 --- /dev/null +++ b/doc/config-examples/test.cfg @@ -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 index 0000000..4227483 --- /dev/null +++ b/doc/schema.sql @@ -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 index 0000000..39477e5 --- /dev/null +++ b/doc/testdata.py @@ -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 index 0000000..e69de29 diff --git a/sercom/config/__init__.py b/sercom/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sercom/config/app.cfg b/sercom/config/app.cfg new file mode 100644 index 0000000..0fd1447 --- /dev/null +++ b/sercom/config/app.cfg @@ -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="" + +# 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 index 0000000..ce776f8 --- /dev/null +++ b/sercom/config/log.cfg @@ -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 index 0000000..4d999ce --- /dev/null +++ b/sercom/controllers.py @@ -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 index 0000000..65838e8 --- /dev/null +++ b/sercom/json.py @@ -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 index 0000000..78772f3 --- /dev/null +++ b/sercom/model.py @@ -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 index 0000000..f0e8327 --- /dev/null +++ b/sercom/release.py @@ -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 index 0000000..1bc7d64 --- /dev/null +++ b/sercom/static/css/style.css @@ -0,0 +1,124 @@ +/* + * Quick mash-up of CSS for the TG quick start page. + */ + +html, body, th, td { + color: black; + background-color: #ddd; + font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif; + margin: 0; + padding: 0; +} + +#header { + height: 80px; + width: 777px; + background: blue URL('../images/header_inner.png') no-repeat; + border-left: 1px solid #aaa; + border-right: 1px solid #aaa; + margin: 0 auto 0 auto; +} + +a.link, a, a.active { + color: #369; +} + + +#main_content { + color: black; + font-size: 127%; + background-color: white; + width: 757px; + margin: 0 auto 0 auto; + border-left: 1px solid #aaa; + border-right: 1px solid #aaa; + padding: 10px; +} + +#sidebar { + border: 1px solid #aaa; + background-color: #eee; + margin: 0.5em; + padding: 1em; + float: right; + width: 200px; + font-size: 88%; +} + +#sidebar h2 { + margin-top: 0; +} + +#sidebar ul { + margin-left: 1.5em; + padding-left: 0; +} + +h1,h2,h3,h4,h5,h6,#getting_started_steps { + font-family: "Century Schoolbook L", Georgia, serif; + font-weight: bold; +} + +h2 { + font-size: 150%; +} + +#getting_started_steps a { + text-decoration: none; +} + +#getting_started_steps a:hover { + text-decoration: underline; +} + +#getting_started_steps li { + font-size: 80%; + margin-bottom: 0.5em; +} + +#getting_started_steps h2 { + font-size: 120%; +} + +#getting_started_steps p { + font: 100% "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif; +} + +#footer { + border: 1px solid #aaa; + border-top: 0px none; + color: #999; + background-color: white; + padding: 10px; + font-size: 80%; + text-align: center; + width: 757px; + margin: 0 auto 1em auto; +} + +.code { + font-family: monospace; +} + +span.code { + font-weight: bold; + background: #eee; +} + +#status_block { + margin: 0 auto 0.5em auto; + padding: 15px 10px 15px 55px; + background: #cec URL('../images/ok.png') left center no-repeat; + border: 1px solid #9c9; + width: 450px; + font-size: 120%; + font-weight: bolder; +} + +.notice { + margin: 0.5em auto 0.5em auto; + padding: 15px 10px 15px 55px; + width: 450px; + background: #eef URL('../images/info.png') left center no-repeat; + border: 1px solid #cce; +} diff --git a/sercom/static/images/favicon.ico b/sercom/static/images/favicon.ico new file mode 100644 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 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 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 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 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 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 index 0000000..e69de29 diff --git a/sercom/templates/login.kid b/sercom/templates/login.kid new file mode 100644 index 0000000..5d7a239 --- /dev/null +++ b/sercom/templates/login.kid @@ -0,0 +1,82 @@ + + + + + + Login + + + + +
+

Identificación

+

Mensaje de error

+

Formulario de login

+
+ + diff --git a/sercom/templates/master.kid b/sercom/templates/master.kid new file mode 100644 index 0000000..7a8e03c --- /dev/null +++ b/sercom/templates/master.kid @@ -0,0 +1,48 @@ + + + + + + + Sercom + + + + + + +
+ + Login + + + Bienvenido ${tg.identity.user.nombre}. + Logout + +
+ +
+
+ +
+ + +
+ + + + diff --git a/sercom/templates/welcome.kid b/sercom/templates/welcome.kid new file mode 100644 index 0000000..d23eb9f --- /dev/null +++ b/sercom/templates/welcome.kid @@ -0,0 +1,48 @@ + + + + +Welcome to TurboGears + + + +
Your application is now running
+ +
+
    +
  1. +

    Model

    +

    Design models in the model.py.
    + Edit dev.cfg to use a different backend, or start with a pre-configured SQLite database.
    + Use script tg-admin sql create to create the database tables.

    +
  2. +
  3. +

    View

    +

    Edit html-like templates in the /templates folder;
    + Put all static contents in the /static folder.

    +
  4. +
  5. +

    Controller

    +

    Edit controllers.py and build your + website structure with the simplicity of Python objects.
    + TurboGears will automatically reload itself when you modify your project.

    +
  6. +
+
If you create something cool, please let people know, and consider contributing something back to the community.
+
+ + + diff --git a/sercom/tests/__init__.py b/sercom/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sercom/tests/test_controllers.py b/sercom/tests/test_controllers.py new file mode 100644 index 0000000..5628e49 --- /dev/null +++ b/sercom/tests/test_controllers.py @@ -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 "Welcome to TurboGears" 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 "Login" 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 index 0000000..2b96ca0 --- /dev/null +++ b/sercom/tests/test_model.py @@ -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 index 0000000..07480d9 --- /dev/null +++ b/sercom_so.egg-info/PKG-INFO @@ -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 index 0000000..7d9e209 --- /dev/null +++ b/sercom_so.egg-info/SOURCES.txt @@ -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 index 0000000..8b13789 --- /dev/null +++ b/sercom_so.egg-info/dependency_links.txt @@ -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 index 0000000..8b13789 --- /dev/null +++ b/sercom_so.egg-info/not-zip-safe @@ -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 index 0000000..14fec70 --- /dev/null +++ b/sercom_so.egg-info/paster_plugins.txt @@ -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 index 0000000..f06697f --- /dev/null +++ b/sercom_so.egg-info/requires.txt @@ -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 index 0000000..04fa0c4 --- /dev/null +++ b/sercom_so.egg-info/sqlobject.txt @@ -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 index 0000000..99331ce --- /dev/null +++ b/sercom_so.egg-info/top_level.txt @@ -0,0 +1 @@ +sercom diff --git a/setup.py b/setup.py new file mode 100644 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 index 0000000..b32eb45 --- /dev/null +++ b/start-sercom.py @@ -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())