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