]> git.llucax.com Git - software/sercom.git/blob - sercom/model.py
a837ca4e714ab1a9df95c4abea68280804bc66ba
[software/sercom.git] / sercom / model.py
1 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker :
2
3 from datetime import datetime
4 from turbogears.database import PackageHub
5 from sqlobject import *
6 from sqlobject.sqlbuilder import *
7 from sqlobject.inheritance import InheritableSQLObject
8 from sqlobject.col import PickleValidator, UnicodeStringValidator
9 from turbogears import identity
10 from turbogears.identity import encrypt_password as encryptpw
11 from sercom.validators import params_to_list, ParseError
12 from formencode import Invalid
13
14 hub = PackageHub("sercom")
15 __connection__ = hub
16
17 __all__ = ('Curso', 'Usuario', 'Docente', 'Alumno', 'Tarea', 'CasoDePrueba')
18
19 #{{{ Custom Columns
20
21 class TupleValidator(PickleValidator):
22     """
23     Validator for tuple types.  A tuple type is simply a pickle type
24     that validates that the represented type is a tuple.
25     """
26     def to_python(self, value, state):
27         value = super(TupleValidator, self).to_python(value, state)
28         if value is None:
29             return None
30         if isinstance(value, tuple):
31             return value
32         raise Invalid("expected a tuple in the TupleCol '%s', got %s %r instead" % \
33             (self.name, type(value), value), value, state)
34     def from_python(self, value, state):
35         if value is None:
36             return None
37         if not isinstance(value, tuple):
38             raise Invalid("expected a tuple in the TupleCol '%s', got %s %r instead" % \
39                 (self.name, type(value), value), value, state)
40         return super(TupleValidator, self).from_python(value, state)
41
42 class SOTupleCol(SOPickleCol):
43     def createValidators(self):
44         return [TupleValidator(name=self.name)] \
45             + super(SOPickleCol, self).createValidators()
46
47 class TupleCol(PickleCol):
48     baseClass = SOTupleCol
49
50 class ParamsValidator(UnicodeStringValidator):
51     def to_python(self, value, state):
52         if isinstance(value, basestring) or value is None:
53             value = super(ParamsValidator, self).to_python(value, state)
54             try:
55                 value = params_to_list(value)
56             except ParseError, e:
57                 raise Invalid("invalid parameters in the ParamsCol '%s', parse "
58                     "error: %s" % (self.name, e), value, state)
59         elif not isinstance(value, (list, tuple)):
60             raise Invalid("expected a tuple, list or valid string in the "
61                 "ParamsCol '%s', got %s %r instead"
62                     % (self.name, type(value), value), value, state)
63         return value
64     def from_python(self, value, state):
65         if isinstance(value, (list, tuple)):
66             value = ' '.join([repr(p) for p in value])
67         elif isinstance(value, basestring) or value is None:
68             value = super(ParamsValidator, self).to_python(value, state)
69             try:
70                 params_to_list(value)
71             except ParseError, e:
72                 raise Invalid("invalid parameters in the ParamsCol '%s', parse "
73                     "error: %s" % (self.name, e), value, state)
74         else:
75             raise Invalid("expected a tuple, list or valid string in the "
76                 "ParamsCol '%s', got %s %r instead"
77                     % (self.name, type(value), value), value, state)
78         return value
79
80 class SOParamsCol(SOUnicodeCol):
81     def createValidators(self):
82         return [ParamsValidator(db_encoding=self.dbEncoding, name=self.name)] \
83             + super(SOParamsCol, self).createValidators()
84
85 class ParamsCol(UnicodeCol):
86     baseClass = SOParamsCol
87
88 #}}}
89
90 #{{{ Tablas intermedias
91
92 # BUG en SQLObject, SQLExpression no tiene cálculo de hash pero se usa como
93 # key de un dict. Workarround hasta que lo arreglen.
94 SQLExpression.__hash__ = lambda self: hash(str(self))
95
96 instancia_tarea_t = table.instancia_tarea
97
98 enunciado_tarea_t = table.enunciado_tarea
99
100 dependencia_t = table.dependencia
101
102 #}}}
103
104 #{{{ Clases
105
106 def srepr(obj): #{{{
107     if obj is not None:
108         return obj.shortrepr()
109     return obj
110 #}}}
111
112 class Curso(SQLObject): #{{{
113     # Clave
114     anio            = IntCol(notNone=True)
115     cuatrimestre    = IntCol(notNone=True)
116     numero          = IntCol(notNone=True)
117     pk              = DatabaseIndex(anio, cuatrimestre, numero, unique=True)
118     # Campos
119     descripcion     = UnicodeCol(length=255, default=None)
120     # Joins
121     docentes        = MultipleJoin('DocenteInscripto')
122     alumnos         = MultipleJoin('AlumnoInscripto')
123     grupos          = MultipleJoin('Grupo')
124     ejercicios      = MultipleJoin('Ejercicio', orderBy='numero')
125
126     def __init__(self, docentes=[], ejercicios=[], alumnos=[], **kw):
127         super(Curso, self).__init__(**kw)
128         for d in docentes:
129             self.add_docente(d)
130         for (n, e) in enumerate(ejercicios):
131             self.add_ejercicio(n, e)
132         for a in alumnos:
133             self.add_alumno(a)
134
135     def set(self, docentes=None, ejercicios=None, alumnos=None, **kw):
136         super(Curso, self).set(**kw)
137         if docentes is not None:
138             for d in DocenteInscripto.selectBy(curso=self):
139                 d.destroySelf()
140             for d in docentes:
141                 self.add_docente(d)
142         if ejercicios is not None:
143             for e in Ejercicio.selectBy(curso=self):
144                 e.destroySelf()
145             for (n, e) in enumerate(ejercicios):
146                 self.add_ejercicio(n, e)
147         if alumnos is not None:
148             for a in AlumnoInscripto.selectBy(curso=self):
149                 a.destroySelf()
150             for a in alumnos:
151                 self.add_alumno(a)
152
153     def add_docente(self, docente, **kw):
154         if isinstance(docente, Docente):
155             kw['docente'] = docente
156         else:
157             kw['docenteID'] = docente
158         return DocenteInscripto(curso=self, **kw)
159
160     def remove_docente(self, docente):
161         if isinstance(docente, Docente):
162             docente = docente.id
163         # FIXME esto deberian arreglarlo en SQLObject y debería ser
164         # DocenteInscripto.pk.get(self, docente).destroySelf()
165         DocenteInscripto.pk.get(self.id, docente).destroySelf()
166
167     def add_alumno(self, alumno, **kw):
168         if isinstance(alumno, Alumno):
169             kw['alumno'] = alumno
170         else:
171             kw['alumnoID'] = alumno
172         return AlumnoInscripto(curso=self, **kw)
173
174     def remove_alumno(self, alumno):
175         if isinstance(alumno, Alumno):
176             alumno = alumno.id
177         # FIXME esto deberian arreglarlo en SQLObject
178         AlumnoInscripto.pk.get(self.id, alumno).destroySelf()
179
180     def add_grupo(self, nombre, **kw):
181         return Grupo(curso=self, nombre=unicode(nombre), **kw)
182
183     def remove_grupo(self, nombre):
184         # FIXME esto deberian arreglarlo en SQLObject
185         Grupo.pk.get(self.id, nombre).destroySelf()
186
187     def add_ejercicio(self, numero, enunciado, **kw):
188         if isinstance(enunciado, Enunciado):
189             kw['enunciado'] = enunciado
190         else:
191             kw['enunciadoID'] = enunciado
192         return Ejercicio(curso=self, numero=numero, **kw)
193
194     def remove_ejercicio(self, numero):
195         # FIXME esto deberian arreglarlo en SQLObject
196         Ejercicio.pk.get(self.id, numero).destroySelf()
197
198     def __repr__(self):
199         return 'Curso(id=%s, anio=%s, cuatrimestre=%s, numero=%s, ' \
200             'descripcion=%s)' \
201                 % (self.id, self.anio, self.cuatrimestre, self.numero,
202                     self.descripcion)
203
204     def shortrepr(self):
205         return '%s.%s.%s' \
206             % (self.anio, self.cuatrimestre, self.numero)
207 #}}}
208
209 class Usuario(InheritableSQLObject): #{{{
210     # Clave (para docentes puede ser un nombre de usuario arbitrario)
211     usuario         = UnicodeCol(length=10, alternateID=True)
212     # Campos
213     contrasenia     = UnicodeCol(length=255, default=None)
214     nombre          = UnicodeCol(length=255, notNone=True)
215     email           = UnicodeCol(length=255, default=None)
216     telefono        = UnicodeCol(length=255, default=None)
217     creado          = DateTimeCol(notNone=True, default=DateTimeCol.now)
218     observaciones   = UnicodeCol(default=None)
219     activo          = BoolCol(notNone=True, default=True)
220     # Joins
221     roles           = RelatedJoin('Rol', addRemoveName='_rol')
222
223     def __init__(self, password=None, roles=[], **kw):
224         if password is not None:
225             kw['contrasenia'] = encryptpw(password)
226         super(Usuario, self).__init__(**kw)
227         for r in roles:
228             self.add_rol(r)
229
230     def set(self, password=None, roles=None, **kw):
231         if password is not None:
232             kw['contrasenia'] = encryptpw(password)
233         super(Usuario, self).set(**kw)
234         if roles is not None:
235             for r in self.roles:
236                 self.remove_rol(r)
237             for r in roles:
238                 self.add_rol(r)
239
240     def _get_user_name(self): # para identity
241         return self.usuario
242
243     @classmethod
244     def by_user_name(cls, user_name): # para identity
245         user = cls.byUsuario(user_name)
246         if not user.activo:
247             raise SQLObjectNotFound, "The object %s with user_name %s is " \
248                 "not active" % (cls.__name__, user_name)
249         return user
250
251     def _get_groups(self): # para identity
252         return self.roles
253
254     def _get_permissions(self): # para identity
255         perms = set()
256         for r in self.roles:
257             perms.update(r.permisos)
258         return perms
259
260     _get_permisos = _get_permissions
261
262     def _set_password(self, cleartext_password): # para identity
263         self.contrasenia = encryptpw(cleartext_password)
264
265     def _get_password(self): # para identity
266         return self.contrasenia
267
268     def __repr__(self):
269         raise NotImplementedError, _('Clase abstracta!')
270
271     def shortrepr(self):
272         return '%s (%s)' % (self.usuario, self.nombre)
273 #}}}
274
275 class Docente(Usuario): #{{{
276     _inheritable = False
277     # Campos
278     nombrado    = BoolCol(notNone=True, default=True)
279     # Joins
280     enunciados  = MultipleJoin('Enunciado', joinColumn='autor_id')
281     cursos      = MultipleJoin('DocenteInscripto')
282
283     def add_entrega(self, instancia, **kw):
284         return Entrega(instancia=instancia, **kw)
285
286     def add_enunciado(self, nombre, anio, cuatrimestre, **kw):
287         return Enunciado(nombre=nombre, anio=anio, cuatrimestre=cuatrimestre,
288             autor=self, **kw)
289
290     def remove_enunciado(self, nombre, anio, cuatrimestre):
291         Enunciado.pk.get(nombre=nombre, anio=anio,
292             cuatrimestre=cuatrimestre).destroySelf()
293
294     def __repr__(self):
295         return 'Docente(id=%s, usuario=%s, nombre=%s, password=%s, email=%s, ' \
296             'telefono=%s, activo=%s, creado=%s, observaciones=%s)' \
297                 % (self.id, self.usuario, self.nombre, self.password,
298                     self.email, self.telefono, self.activo, self.creado,
299                     self.observaciones)
300 #}}}
301
302 class Alumno(Usuario): #{{{
303     _inheritable = False
304     # Campos
305     nota            = DecimalCol(size=3, precision=1, default=None)
306     # Joins
307     inscripciones   = MultipleJoin('AlumnoInscripto')
308
309     def __init__(self, padron=None, **kw):
310         if padron: kw['usuario'] = padron
311         super(Alumno, self).__init__(**kw)
312
313     def set(self, padron=None, **kw):
314         if padron: kw['usuario'] = padron
315         super(Alumno, self).set(**kw)
316
317     def _get_padron(self): # alias para poder referirse al alumno por padron
318         return self.usuario
319
320     def _set_padron(self, padron):
321         self.usuario = padron
322
323     @classmethod
324     def byPadron(cls, padron):
325         return cls.byUsuario(unicode(padron))
326
327     def __repr__(self):
328         return 'Alumno(id=%s, padron=%s, nombre=%s, password=%s, email=%s, ' \
329             'telefono=%s, activo=%s, creado=%s, observaciones=%s)' \
330                 % (self.id, self.padron, self.nombre, self.password, self.email,
331                     self.telefono, self.activo, self.creado, self.observaciones)
332 #}}}
333
334 class Tarea(InheritableSQLObject): #{{{
335     class sqlmeta:
336         createSQL = dict(sqlite=r'''
337 CREATE TABLE dependencia (
338     padre_id INTEGER NOT NULL CONSTRAINT tarea_id_exists
339         REFERENCES tarea(id) ON DELETE CASCADE,
340     hijo_id INTEGER NOT NULL CONSTRAINT tarea_id_exists
341         REFERENCES tarea(id) ON DELETE CASCADE,
342     orden INT,
343     PRIMARY KEY (padre_id, hijo_id)
344 )''')
345     # Clave
346     nombre          = UnicodeCol(length=30, alternateID=True)
347     # Campos
348     descripcion     = UnicodeCol(length=255, default=None)
349     # Joins
350
351     def __init__(self, dependencias=(), **kw):
352         super(Tarea, self).__init__(**kw)
353         if dependencias:
354             self.dependencias = dependencias
355
356     def set(self, dependencias=None, **kw):
357         super(Tarea, self).set(**kw)
358         if dependencias is not None:
359             self.dependencias = dependencias
360
361     def _get_dependencias(self):
362         OtherTarea = Alias(Tarea, 'other_tarea')
363         self.__dependencias = tuple(Tarea.select(
364             AND(
365                 Tarea.q.id == dependencia_t.hijo_id,
366                 OtherTarea.q.id == dependencia_t.padre_id,
367                 self.id == dependencia_t.padre_id,
368             ),
369             clauseTables=(dependencia_t,),
370             orderBy=dependencia_t.orden,
371         ))
372         return self.__dependencias
373
374     def _set_dependencias(self, dependencias):
375         orden = {}
376         for i, t in enumerate(dependencias):
377             orden[t.id] = i
378         new = frozenset([t.id for t in dependencias])
379         old = frozenset([t.id for t in self.dependencias])
380         dependencias = dict([(t.id, t) for t in dependencias])
381         for tid in old - new: # eliminadas
382             self._connection.query(str(Delete(dependencia_t, where=AND(
383                 dependencia_t.padre_id == self.id,
384                 dependencia_t.hijo_id == tid))))
385         for tid in new - old: # creadas
386             self._connection.query(str(Insert(dependencia_t, values=dict(
387                 padre_id=self.id, hijo_id=tid, orden=orden[tid]
388             ))))
389         for tid in new & old: # actualizados
390             self._connection.query(str(Update(dependencia_t,
391                 values=dict(orden=orden[tid]), where=AND(
392                     dependencia_t.padre_id == self.id,
393                     dependencia_t.hijo_id == tid,
394                 ))))
395
396     def __repr__(self):
397         return 'Tarea(id=%s, nombre=%s, descripcion=%s)' \
398                 % (self.id, self.nombre, self.descripcion)
399
400     def shortrepr(self):
401         return self.nombre
402 #}}}
403
404 class Enunciado(SQLObject): #{{{
405     class sqlmeta:
406         createSQL = dict(sqlite=r'''
407 CREATE TABLE enunciado_tarea (
408     enunciado_id INTEGER NOT NULL CONSTRAINT enunciado_id_exists
409         REFERENCES enunciado(id) ON DELETE CASCADE,
410     tarea_id INTEGER NOT NULL CONSTRAINT tarea_id_exists
411         REFERENCES tarea(id) ON DELETE CASCADE,
412     orden INT,
413     PRIMARY KEY (enunciado_id, tarea_id)
414 )''')
415     # Clave
416     nombre          = UnicodeCol(length=60)
417     anio            = IntCol(notNone=True)
418     cuatrimestre    = IntCol(notNone=True)
419     pk              = DatabaseIndex(nombre, anio, cuatrimestre, unique=True)
420     # Campos
421     autor           = ForeignKey('Docente', cascade='null')
422     descripcion     = UnicodeCol(length=255, default=None)
423     creado          = DateTimeCol(notNone=True, default=DateTimeCol.now)
424     archivo         = BLOBCol(default=None)
425     archivo_name    = UnicodeCol(length=255, default=None)
426     archivo_type    = UnicodeCol(length=255, default=None)
427     # Joins
428     ejercicios      = MultipleJoin('Ejercicio')
429     casos_de_prueba = MultipleJoin('CasoDePrueba')
430
431     def __init__(self, tareas=(), **kw):
432         super(Enunciado, self).__init__(**kw)
433         if tareas:
434             self.tareas = tareas
435
436     def set(self, tareas=None, **kw):
437         super(Enunciado, self).set(**kw)
438         if tareas is not None:
439             self.tareas = tareas
440
441     @classmethod
442     def selectByCurso(self, curso):
443         return Enunciado.selectBy(cuatrimestre=curso.cuatrimestre, anio=curso.anio)
444
445     def add_caso_de_prueba(self, nombre, **kw):
446         return CasoDePrueba(enunciado=self, nombre=nombre, **kw)
447
448     def _get_tareas(self):
449         self.__tareas = tuple(Tarea.select(
450             AND(
451                 Tarea.q.id == enunciado_tarea_t.tarea_id,
452                 Enunciado.q.id == enunciado_tarea_t.enunciado_id,
453                 Enunciado.q.id == self.id
454             ),
455             clauseTables=(enunciado_tarea_t, Enunciado.sqlmeta.table),
456             orderBy=enunciado_tarea_t.orden,
457         ))
458         return self.__tareas
459
460     def _set_tareas(self, tareas):
461         orden = {}
462         for i, t in enumerate(tareas):
463             orden[t.id] = i
464         new = frozenset([t.id for t in tareas])
465         old = frozenset([t.id for t in self.tareas])
466         tareas = dict([(t.id, t) for t in tareas])
467         for tid in old - new: # eliminadas
468             self._connection.query(str(Delete(enunciado_tarea_t, where=AND(
469                 enunciado_tarea_t.enunciado_id == self.id,
470                 enunciado_tarea_t.tarea_id == tid))))
471         for tid in new - old: # creadas
472             self._connection.query(str(Insert(enunciado_tarea_t, values=dict(
473                 enunciado_id=self.id, tarea_id=tid, orden=orden[tid]
474             ))))
475         for tid in new & old: # actualizados
476             self._connection.query(str(Update(enunciado_tarea_t,
477                 values=dict(orden=orden[tid]), where=AND(
478                     enunciado_tarea_t.enunciado_id == self.id,
479                     enunciado_tarea_t.tarea_id == tid,
480                 ))))
481
482     def __repr__(self):
483         return 'Enunciado(id=%s, autor=%s, nombre=%s, descripcion=%s, ' \
484             'creado=%s)' \
485                 % (self.id, srepr(self.autor), self.nombre, self.descripcion, \
486                     self.creado)
487
488     def shortrepr(self):
489         return self.nombre
490 #}}}
491
492 class CasoDePrueba(SQLObject): #{{{
493     # Clave
494     enunciado       = ForeignKey('Enunciado', cascade=True)
495     nombre          = UnicodeCol(length=40, notNone=True)
496     pk              = DatabaseIndex(enunciado, nombre, unique=True)
497     # Campos
498     privado         = IntCol(default=None) # TODO iria en instancia_de_entrega_caso_de_prueba
499     parametros      = ParamsCol(length=255, default=None)
500     retorno         = IntCol(default=None)
501     tiempo_cpu      = FloatCol(default=None)
502     descripcion     = UnicodeCol(length=255, default=None)
503     activo          = BoolCol(notNone=True, default=True)
504     # Joins
505     pruebas         = MultipleJoin('Prueba')
506
507     def __repr__(self):
508         return 'CasoDePrueba(enunciado=%s, nombre=%s, parametros=%s, ' \
509             'retorno=%s, tiempo_cpu=%s, descripcion=%s)' \
510                 % (srepr(self.enunciado), self.nombre, self.parametros,
511                     self.retorno, self.tiempo_cpu, self.descripcion)
512
513     def shortrepr(self):
514         return '%s:%s' % (self.enunciado.shortrepr(), self.nombre)
515 #}}}
516
517 class Ejercicio(SQLObject): #{{{
518     # Clave
519     curso           = ForeignKey('Curso', notNone=True, cascade=True)
520     numero          = IntCol(notNone=True)
521     pk              = DatabaseIndex(curso, numero, unique=True)
522     # Campos
523     enunciado       = ForeignKey('Enunciado', notNone=True, cascade=False)
524     grupal          = BoolCol(notNone=True, default=False)
525     # Joins
526     instancias      = MultipleJoin('InstanciaDeEntrega')
527
528     def add_instancia(self, numero, inicio, fin, **kw):
529         return InstanciaDeEntrega(ejercicio=self, numero=numero, inicio=inicio,
530             fin=fin, **kw)
531
532     def remove_instancia(self, numero):
533         InstanciaDeEntrega.pk.get(ejercicio=self, numero=numero).destroySelf()
534
535     def __repr__(self):
536         return 'Ejercicio(id=%s, curso=%s, numero=%s, enunciado=%s, ' \
537             'grupal=%s)' \
538                 % (self.id, self.curso.shortrepr(), self.numero,
539                     self.enunciado.shortrepr(), self.grupal)
540
541     def shortrepr(self):
542         return '(%s, %s, %s)' \
543             % (self.curso.shortrepr(), str(self.numero), \
544                 self.enunciado.shortrepr())
545 #}}}
546
547 class InstanciaDeEntrega(SQLObject): #{{{
548     class sqlmeta:
549         createSQL = dict(sqlite=r'''
550 CREATE TABLE instancia_tarea (
551     instancia_id INTEGER NOT NULL CONSTRAINT instancia_id_exists
552         REFERENCES instancia_de_entrega(id) ON DELETE CASCADE,
553     tarea_id INTEGER NOT NULL CONSTRAINT tarea_id_exists
554         REFERENCES tarea(id) ON DELETE CASCADE,
555     orden INT,
556     PRIMARY KEY (instancia_id, tarea_id)
557 )''')
558     # Clave
559     ejercicio       = ForeignKey('Ejercicio', notNone=True, cascade=True)
560     numero          = IntCol(notNone=True)
561     pk              = DatabaseIndex(ejercicio, numero, unique=True)
562     # Campos
563     inicio          = DateTimeCol(notNone=True)
564     fin             = DateTimeCol(notNone=True)
565     procesada       = BoolCol(notNone=True, default=False)
566     observaciones   = UnicodeCol(default=None)
567     activo          = BoolCol(notNone=True, default=True)
568     # Joins
569     entregas        = MultipleJoin('Entrega', joinColumn='instancia_id')
570     correcciones    = MultipleJoin('Correccion', joinColumn='instancia_id')
571
572     def __init__(self, tareas=(), **kw):
573         super(InstanciaDeEntrega, self).__init__(**kw)
574         if tareas:
575             self.tareas = tareas
576
577     def set(self, tareas=None, **kw):
578         super(InstanciaDeEntrega, self).set(**kw)
579         if tareas is not None:
580             self.tareas = tareas
581
582     def _get_tareas(self):
583         self.__tareas = tuple(Tarea.select(
584             AND(
585                 Tarea.q.id == instancia_tarea_t.tarea_id,
586                 InstanciaDeEntrega.q.id == instancia_tarea_t.instancia_id,
587                 InstanciaDeEntrega.q.id == self.id,
588             ),
589             clauseTables=(instancia_tarea_t, InstanciaDeEntrega.sqlmeta.table),
590             orderBy=instancia_tarea_t.orden,
591         ))
592         return self.__tareas
593
594     def _set_tareas(self, tareas):
595         orden = {}
596         for i, t in enumerate(tareas):
597             orden[t.id] = i
598         new = frozenset([t.id for t in tareas])
599         old = frozenset([t.id for t in self.tareas])
600         tareas = dict([(t.id, t) for t in tareas])
601         for tid in old - new: # eliminadas
602             self._connection.query(str(Delete(instancia_tarea_t, where=AND(
603                 instancia_tarea_t.instancia_id == self.id,
604                 instancia_tarea_t.tarea_id == tid))))
605         for tid in new - old: # creadas
606             self._connection.query(str(Insert(instancia_tarea_t, values=dict(
607                 instancia_id=self.id, tarea_id=tid, orden=orden[tid]
608             ))))
609         for tid in new & old: # actualizados
610             self._connection.query(str(Update(instancia_tarea_t,
611                 values=dict(orden=orden[tid]), where=AND(
612                     instancia_tarea_t.instancia_id == self.id,
613                     instancia_tarea_t.tarea_id == tid,
614                 ))))
615
616     def __repr__(self):
617         return 'InstanciaDeEntrega(id=%s, numero=%s, inicio=%s, fin=%s, ' \
618             'procesada=%s, observaciones=%s, activo=%s)' \
619                 % (self.id, self.numero, self.inicio, self.fin,
620                     self.procesada, self.observaciones, self.activo)
621
622     def shortrepr(self):
623         return self.numero
624 #}}}
625
626 class DocenteInscripto(SQLObject): #{{{
627     # Clave
628     curso           = ForeignKey('Curso', notNone=True, cascade=True)
629     docente         = ForeignKey('Docente', notNone=True, cascade=True)
630     pk              = DatabaseIndex(curso, docente, unique=True)
631     # Campos
632     corrige         = BoolCol(notNone=True, default=True)
633     observaciones   = UnicodeCol(default=None)
634     # Joins
635     alumnos         = MultipleJoin('AlumnoInscripto', joinColumn='tutor_id')
636     tutorias        = MultipleJoin('Tutor', joinColumn='docente_id')
637     correcciones    = MultipleJoin('Correccion', joinColumn='corrector_id')
638
639     def add_correccion(self, entrega, **kw):
640         return Correccion(instancia=entrega.instancia, entrega=entrega,
641             entregador=entrega.entregador, corrector=self, **kw)
642
643     def remove_correccion(self, instancia, entregador):
644         Correccion.pk.get(instancia=instancia,
645             entregador=entregador).destroySelf()
646
647     def __repr__(self):
648         return 'DocenteInscripto(id=%s, docente=%s, corrige=%s, ' \
649             'observaciones=%s' \
650                 % (self.id, self.docente.shortrepr(), self.corrige,
651                     self.observaciones)
652
653     def shortrepr(self):
654         return self.docente.shortrepr()
655 #}}}
656
657 class Entregador(InheritableSQLObject): #{{{
658     # Campos
659     nota            = DecimalCol(size=3, precision=1, default=None)
660     nota_cursada    = DecimalCol(size=3, precision=1, default=None)
661     observaciones   = UnicodeCol(default=None)
662     activo          = BoolCol(notNone=True, default=True)
663     # Joins
664     entregas        = MultipleJoin('Entrega')
665     correcciones    = MultipleJoin('Correccion')
666
667     def add_entrega(self, instancia, **kw):
668         return Entrega(instancia=instancia, entregador=self, **kw)
669
670     def __repr__(self):
671         raise NotImplementedError, 'Clase abstracta!'
672 #}}}
673
674 class Grupo(Entregador): #{{{
675     _inheritable = False
676     # Clave
677     curso           = ForeignKey('Curso', notNone=True, cascade=True)
678     nombre          = UnicodeCol(length=20, notNone=True)
679     pk              = DatabaseIndex(curso, nombre, unique=True)
680     # Campos
681     responsable     = ForeignKey('AlumnoInscripto', default=None, cascade='null')
682     # Joins
683     miembros        = MultipleJoin('Miembro')
684     tutores         = MultipleJoin('Tutor')
685
686     def __init__(self, miembros=[], tutores=[], **kw):
687         super(Grupo, self).__init__(**kw)
688         for a in miembros:
689             self.add_miembro(a)
690         for d in tutores:
691             self.add_tutor(d)
692
693     def set(self, miembros=None, tutores=None, **kw):
694         super(Grupo, self).set(**kw)
695         if miembros is not None:
696             for m in Miembro.selectBy(grupo=self):
697                 m.destroySelf()
698             for m in miembros:
699                 self.add_miembro(m)
700         if tutores is not None:
701             for t in Tutor.selectBy(grupo=self):
702                 t.destroySelf()
703             for t in tutores:
704                 self.add_tutor(t)
705
706     def add_miembro(self, alumno, **kw):
707         if isinstance(alumno, AlumnoInscripto):
708             kw['alumno'] = alumno
709         else:
710             kw['alumnoID'] = alumno
711         return Miembro(grupo=self, **kw)
712
713     def remove_miembro(self, alumno):
714         if isinstance(alumno, AlumnoInscripto):
715             Miembro.pk.get(grupo=self, alumno=alumno).destroySelf()
716         else:
717             Miembro.pk.get(grupo=self, alumnoID=alumno).destroySelf()
718
719     def add_tutor(self, docente, **kw):
720         if isinstance(docente, DocenteInscripto):
721             kw['docente'] = docente
722         else:
723             kw['docenteID'] = docente
724         return Tutor(grupo=self, **kw)
725
726     def remove_tutor(self, docente):
727         if isinstance(docente, DocenteInscripto):
728             Tutor.pk.get(grupo=self, docente=docente).destroySelf()
729         else:
730             Tutor.pk.get(grupo=self, docenteID=docente).destroySelf()
731
732     def __repr__(self):
733         return 'Grupo(id=%s, nombre=%s, responsable=%s, nota=%s, ' \
734             'nota_cursada=%s, observaciones=%s, activo=%s)' \
735                 % (self.id, self.nombre, srepr(self.responsable), self.nota,
736                     self.nota_cursada, self.observaciones, self.activo)
737
738     def shortrepr(self):
739         return 'grupo:' + self.nombre
740 #}}}
741
742 class AlumnoInscripto(Entregador): #{{{
743     _inheritable = False
744     # Clave
745     curso               = ForeignKey('Curso', notNone=True, cascade=True)
746     alumno              = ForeignKey('Alumno', notNone=True, cascade=True)
747     pk                  = DatabaseIndex(curso, alumno, unique=True)
748     # Campos
749     condicional         = BoolCol(notNone=True, default=False)
750     tutor               = ForeignKey('DocenteInscripto', default=None, cascade='null')
751     # Joins
752     responsabilidades   = MultipleJoin('Grupo', joinColumn='responsable_id')
753     membresias          = MultipleJoin('Miembro', joinColumn='alumno_id')
754     entregas            = MultipleJoin('Entrega', joinColumn='alumno_id')
755     correcciones        = MultipleJoin('Correccion', joinColumn='alumno_id')
756
757     def __repr__(self):
758         return 'AlumnoInscripto(id=%s, alumno=%s, condicional=%s, nota=%s, ' \
759             'nota_cursada=%s, tutor=%s, observaciones=%s, activo=%s)' \
760                 % (self.id, self.alumno.shortrepr(), self.condicional,
761                     self.nota, self.nota_cursada, srepr(self.tutor),
762                     self.observaciones, self.activo)
763
764     def shortrepr(self):
765         return self.alumno.shortrepr()
766 #}}}
767
768 class Tutor(SQLObject): #{{{
769     # Clave
770     grupo           = ForeignKey('Grupo', notNone=True, cascade=True)
771     docente         = ForeignKey('DocenteInscripto', notNone=True, cascade=True)
772     pk              = DatabaseIndex(grupo, docente, unique=True)
773     # Campos
774     alta            = DateTimeCol(notNone=True, default=DateTimeCol.now)
775     baja            = DateTimeCol(default=None)
776
777     def __repr__(self):
778         return 'Tutor(docente=%s, grupo=%s, alta=%s, baja=%s)' \
779                 % (self.docente.shortrepr(), self.grupo.shortrepr(),
780                     self.alta, self.baja)
781
782     def shortrepr(self):
783         return '%s-%s' % (self.docente.shortrepr(), self.grupo.shortrepr())
784 #}}}
785
786 class Miembro(SQLObject): #{{{
787     # Clave
788     grupo           = ForeignKey('Grupo', notNone=True, cascade=True)
789     alumno          = ForeignKey('AlumnoInscripto', notNone=True, cascade=True)
790     pk              = DatabaseIndex(grupo, alumno, unique=True)
791     # Campos
792     nota            = DecimalCol(size=3, precision=1, default=None)
793     alta            = DateTimeCol(notNone=True, default=DateTimeCol.now)
794     baja            = DateTimeCol(default=None)
795
796     def __repr__(self):
797         return 'Miembro(alumno=%s, grupo=%s, nota=%s, alta=%s, baja=%s)' \
798                 % (self.alumno.shortrepr(), self.grupo.shortrepr(),
799                     self.nota, self.alta, self.baja)
800
801     def shortrepr(self):
802         return '%s-%s' % (self.alumno.shortrepr(), self.grupo.shortrepr())
803 #}}}
804
805 class Entrega(SQLObject): #{{{
806     # Clave
807     instancia       = ForeignKey('InstanciaDeEntrega', notNone=True, cascade=False)
808     entregador      = ForeignKey('Entregador', default=None, cascade=False) # Si es None era un Docente
809     fecha           = DateTimeCol(notNone=True, default=DateTimeCol.now)
810     pk              = DatabaseIndex(instancia, entregador, fecha, unique=True)
811     # Campos
812     correcta        = BoolCol(notNone=True, default=False)
813     observaciones   = UnicodeCol(default=None)
814     # Joins
815     tareas          = MultipleJoin('TareaEjecutada')
816     # Para generar código
817     codigo_dict     = r'0123456789abcdefghijklmnopqrstuvwxyz_.,*@#+'
818     codigo_format   = r'%m%d%H%M%S'
819
820     def add_tarea_ejecutada(self, tarea, **kw):
821         return TareaEjecutada(tarea=tarea, entrega=self, **kw)
822
823     def _get_codigo(self):
824         if not hasattr(self, '_codigo'): # cache
825             n = long(self.fecha.strftime(Entrega.codigo_format))
826             d = Entrega.codigo_dict
827             l = len(d)
828             res = ''
829             while n:
830                     res += d[n % l]
831                     n /= l
832             self._codigo = res
833         return self._codigo
834
835     def _set_fecha(self, fecha):
836         self._SO_set_fecha(fecha)
837         if hasattr(self, '_codigo'): del self._codigo # bye, bye cache!
838
839     def __repr__(self):
840         return 'Entrega(instancia=%s, entregador=%s, codigo=%s, fecha=%s, ' \
841             'correcta=%s, observaciones=%s)' \
842                 % (self.instancia.shortrepr(), srepr(self.entregador),
843                     self.codigo, self.fecha, self.correcta, self.observaciones)
844
845     def shortrepr(self):
846         return '%s-%s-%s' % (self.instancia.shortrepr(), srepr(self.entregador),
847             self.codigo)
848 #}}}
849
850 class Correccion(SQLObject): #{{{
851     # Clave
852     instancia       = ForeignKey('InstanciaDeEntrega', notNone=True, cascade=False)
853     entregador      = ForeignKey('Entregador', notNone=True, cascade=False) # Docente no tiene
854     pk              = DatabaseIndex(instancia, entregador, unique=True)
855     # Campos
856     entrega         = ForeignKey('Entrega', notNone=True, cascade=False)
857     corrector       = ForeignKey('DocenteInscripto', default=None, cascade='null')
858     asignado        = DateTimeCol(notNone=True, default=DateTimeCol.now)
859     corregido       = DateTimeCol(default=None)
860     nota            = DecimalCol(size=3, precision=1, default=None)
861     observaciones   = UnicodeCol(default=None)
862
863     def _get_entregas(self):
864         return list(Entrega.selectBy(instancia=self.instancia, entregador=self.entregador))
865
866     def __repr__(self):
867         return 'Correccion(instancia=%s, entregador=%s, entrega=%s, ' \
868             'corrector=%s, asignado=%s, corregido=%s, nota=%s, ' \
869             'observaciones=%s)' \
870                 % (self.instancia.shortrepr(), self.entregador.shortrepr(),
871                     self.entrega.shortrepr(), self.corrector, self.asignado,
872                     self.corregido, self.nota, self.observaciones)
873
874     def shortrepr(self):
875         if not self.corrector:
876             return '%s' % self.entrega.shortrepr()
877         return '%s,%s' % (self.entrega.shortrepr(), self.corrector.shortrepr())
878 #}}}
879
880 class TareaEjecutada(InheritableSQLObject): #{{{
881     # Clave
882     tarea           = ForeignKey('Tarea', notNone=True, cascade=False)
883     entrega         = ForeignKey('Entrega', notNone=True, cascade=False)
884     pk              = DatabaseIndex(tarea, entrega, unique=True)
885     # Campos
886     inicio          = DateTimeCol(notNone=True, default=DateTimeCol.now)
887     fin             = DateTimeCol(default=None)
888     exito           = IntCol(default=None)
889     observaciones   = UnicodeCol(default=None)
890     # Joins
891     pruebas         = MultipleJoin('Prueba')
892
893     def add_prueba(self, caso_de_prueba, **kw):
894         return Prueba(tarea_ejecutada=self, caso_de_prueba=caso_de_prueba,
895             **kw)
896
897     def __repr__(self):
898         return 'TareaEjecutada(tarea=%s, entrega=%s, inicio=%s, fin=%s, ' \
899             'exito=%s, observaciones=%s)' \
900                 % (self.tarea.shortrepr(), self.entrega.shortrepr(),
901                     self.inicio, self.fin, self.exito, self.observaciones)
902
903     def shortrepr(self):
904         return '%s-%s' % (self.tarea.shortrepr(), self.entrega.shortrepr())
905 #}}}
906
907 class Prueba(SQLObject): #{{{
908     # Clave
909     tarea_ejecutada = ForeignKey('TareaEjecutada', notNone=True, cascade=False)
910     caso_de_prueba  = ForeignKey('CasoDePrueba', notNone=True, cascade=False)
911     pk              = DatabaseIndex(tarea_ejecutada, caso_de_prueba, unique=True)
912     # Campos
913     inicio          = DateTimeCol(notNone=True, default=DateTimeCol.now)
914     fin             = DateTimeCol(default=None)
915     pasada          = IntCol(default=None)
916     observaciones   = UnicodeCol(default=None)
917
918     def __repr__(self):
919         return 'Prueba(tarea_ejecutada=%s, caso_de_prueba=%s, inicio=%s, ' \
920             'fin=%s, pasada=%s, observaciones=%s)' \
921                 % (self.tarea_ejecutada.shortrepr(),
922                     self.caso_de_prueba.shortrepr(), self.inicio, self.fin,
923                     self.pasada, self.observaciones)
924
925     def shortrepr(self):
926         return '%s:%s' % (self.tarea_ejecutada.shortrepr(),
927             self.caso_de_prueba.shortrerp())
928 #}}}
929
930 #{{{ Específico de Identity
931
932 class Visita(SQLObject): #{{{
933     visit_key   = StringCol(length=40, alternateID=True,
934                     alternateMethodName="by_visit_key")
935     created     = DateTimeCol(notNone=True, default=datetime.now)
936     expiry      = DateTimeCol()
937
938     @classmethod
939     def lookup_visit(cls, visit_key):
940         try:
941             return cls.by_visit_key(visit_key)
942         except SQLObjectNotFound:
943             return None
944 #}}}
945
946 class VisitaUsuario(SQLObject): #{{{
947     # Clave
948     visit_key   = StringCol(length=40, alternateID=True,
949                           alternateMethodName="by_visit_key")
950     # Campos
951     user_id     = IntCol() # Negrada de identity
952 #}}}
953
954 class Rol(SQLObject): #{{{
955     # Clave
956     nombre      = UnicodeCol(length=255, alternateID=True,
957                     alternateMethodName='by_nombre')
958     # Campos
959     descripcion = UnicodeCol(length=255, default=None)
960     creado      = DateTimeCol(notNone=True, default=datetime.now)
961     permisos    = TupleCol(notNone=True)
962     # Joins
963     usuarios    = RelatedJoin('Usuario', addRemoveName='_usuario')
964
965     def by_group_name(self, name): # para identity
966         return self.by_nombre(name)
967 #}}}
968
969 # No es un SQLObject porque no tiene sentido agregar/sacar permisos, están
970 # hardcodeados en el código
971 class Permiso(object): #{{{
972     max_valor = 1
973     def __init__(self, nombre, descripcion):
974         self.valor = Permiso.max_valor
975         Permiso.max_valor <<= 1
976         self.nombre = nombre
977         self.descripcion = descripcion
978
979     @classmethod
980     def createTable(cls, ifNotExists): # para identity
981         pass
982
983     @property
984     def permission_name(self): # para identity
985         return self.nombre
986
987     def __and__(self, other):
988         return self.valor & other.valor
989
990     def __or__(self, other):
991         return self.valor | other.valor
992
993     def __repr__(self):
994         return self.nombre
995 #}}}
996
997 # TODO ejemplos
998 entregar_tp = Permiso(u'entregar', u'Permite entregar trabajos prácticos')
999 admin = Permiso(u'admin', u'Permite hacer ABMs arbitrarios')
1000
1001 #}}} Identity
1002
1003 #}}} Clases
1004