From 2344dcf6736372c1dd359a95819fb3c2e131afc2 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 26 Jan 2007 05:45:41 +0000 Subject: [PATCH] =?utf8?q?Import=20inicial.=20Estructura=20b=C3=A1sica=20d?= =?utf8?q?el=20proyecto=20con=20el=20modelo=20de=20datos=20te=C3=B3ricamen?= =?utf8?q?te=20terminado.?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- README.txt | 4 + doc/config-examples/dev.cfg | 65 ++ doc/config-examples/sample-prod.cfg | 78 ++ doc/config-examples/test.cfg | 4 + doc/schema.sql | 218 ++++++ doc/testdata.py | 65 ++ sercom/__init__.py | 0 sercom/config/__init__.py | 0 sercom/config/app.cfg | 128 ++++ sercom/config/log.cfg | 29 + sercom/controllers.py | 75 ++ sercom/json.py | 33 + sercom/model.py | 718 +++++++++++++++++++ sercom/release.py | 14 + sercom/static/css/style.css | 124 ++++ sercom/static/images/favicon.ico | Bin 0 -> 1081 bytes sercom/static/images/header_inner.png | Bin 0 -> 37537 bytes sercom/static/images/info.png | Bin 0 -> 2889 bytes sercom/static/images/ok.png | Bin 0 -> 25753 bytes sercom/static/images/tg_under_the_hood.png | Bin 0 -> 4010 bytes sercom/static/images/under_the_hood_blue.png | Bin 0 -> 2667 bytes sercom/templates/__init__.py | 0 sercom/templates/login.kid | 82 +++ sercom/templates/master.kid | 48 ++ sercom/templates/welcome.kid | 48 ++ sercom/tests/__init__.py | 0 sercom/tests/test_controllers.py | 33 + sercom/tests/test_model.py | 22 + sercom_so.egg-info/PKG-INFO | 15 + sercom_so.egg-info/SOURCES.txt | 21 + sercom_so.egg-info/dependency_links.txt | 1 + sercom_so.egg-info/not-zip-safe | 1 + sercom_so.egg-info/paster_plugins.txt | 2 + sercom_so.egg-info/requires.txt | 1 + sercom_so.egg-info/sqlobject.txt | 2 + sercom_so.egg-info/top_level.txt | 1 + setup.py | 62 ++ start-sercom.py | 25 + 38 files changed, 1919 insertions(+) create mode 100644 README.txt create mode 100644 doc/config-examples/dev.cfg create mode 100644 doc/config-examples/sample-prod.cfg create mode 100644 doc/config-examples/test.cfg create mode 100644 doc/schema.sql create mode 100644 doc/testdata.py create mode 100644 sercom/__init__.py create mode 100644 sercom/config/__init__.py create mode 100644 sercom/config/app.cfg create mode 100644 sercom/config/log.cfg create mode 100644 sercom/controllers.py create mode 100644 sercom/json.py create mode 100644 sercom/model.py create mode 100644 sercom/release.py create mode 100644 sercom/static/css/style.css create mode 100644 sercom/static/images/favicon.ico create mode 100644 sercom/static/images/header_inner.png create mode 100644 sercom/static/images/info.png create mode 100644 sercom/static/images/ok.png create mode 100644 sercom/static/images/tg_under_the_hood.png create mode 100644 sercom/static/images/under_the_hood_blue.png create mode 100644 sercom/templates/__init__.py create mode 100644 sercom/templates/login.kid create mode 100644 sercom/templates/master.kid create mode 100644 sercom/templates/welcome.kid create mode 100644 sercom/tests/__init__.py create mode 100644 sercom/tests/test_controllers.py create mode 100644 sercom/tests/test_model.py create mode 100644 sercom_so.egg-info/PKG-INFO create mode 100644 sercom_so.egg-info/SOURCES.txt create mode 100644 sercom_so.egg-info/dependency_links.txt create mode 100644 sercom_so.egg-info/not-zip-safe create mode 100644 sercom_so.egg-info/paster_plugins.txt create mode 100644 sercom_so.egg-info/requires.txt create mode 100644 sercom_so.egg-info/sqlobject.txt create mode 100644 sercom_so.egg-info/top_level.txt create mode 100644 setup.py create mode 100644 start-sercom.py 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 0000000000000000000000000000000000000000..332557bc307647601389c14939be0671c62efcd7 GIT binary patch literal 1081 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 literal 0 HcmV?d00001 diff --git a/sercom/static/images/ok.png b/sercom/static/images/ok.png new file mode 100644 index 0000000000000000000000000000000000000000..fee6751c3dd915e55da53ed6a1e7b5809cc76b63 GIT binary patch literal 25753 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bc9c79cc6141a67a21edaffc9a068ebd0899eb46 GIT binary patch literal 4010 zcmV;b4^{AqP)*YDvbIYV`E zcS*)_N8;Vt-{0T4i*l)rclP%7^78WR?CkR9;P$YDnnM-rm~X_Hf?6gCgI!D3*xIvn zQ{CO&=H}+Sp@`Jf)V8Uj(#^}_=IZF^=*z;rmu*C&dQ{-x;HiUM_4)eK$-?jO@apR7 z4`B0d{{=xteoB8;=Pe*n1FP(i(`00 zCd0L>(Y~|l?(u6oA$UtP`27CBuA8AxB)zPixrJ$+S~$(6kGzasu$G0tl6vUv@2XQK zfQ>0ljtdM-Vqm#KFMX+T796(dp~%*3;2vD+#Qelyox?%EiL-_V~t;T(y5=&(hTV{{OkB zo72X-v!k2Uw4&C#tI^Wbv{xzOdCvVzK(S3?e5yiz@JVdkvR$b{r=6)&)weN*q>+N;^X!7@z~DD_xk&9Kqikt z9OvrnwqG-EM=`>±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 + + + + + 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 + + + + + + +
+ +
+
+ +
+ + +
+ + + + 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()) -- 2.43.0
+ + Login + + + Bienvenido ${tg.identity.user.nombre}. + Logout + +