Montrer les notices les plus récentes dans la page d'accueil.
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
CommitLineData
92c7413b 1# -*- encoding: utf-8 -*-
5212238e
EMS
2import caldav
3import datetime
4import operator
5import os
6import pytz
7import random
8import simplejson
9import time
10import uuid
11import vobject
12from backend_config import RESOURCES
86983865 13from babel.dates import get_timezone_name
5212238e
EMS
14from caldav.lib import error
15from datamaster_modeles.models import Thematique, Pays, Region
6d885e0c 16from django.contrib.auth.models import User
d15017b2 17from django.db import models
15261361 18from django.db.models import Q, Max
b7a741ad 19from django.db.models.signals import pre_delete
5212238e
EMS
20from django.utils.encoding import smart_unicode
21from djangosphinx.models import SphinxQuerySet
da9020f3 22from savoirs.globals import META
5212238e 23from savoirs.lib.calendrier import combine
74b087e5 24from settings import CALENDRIER_URL, SITE_ROOT_URL
5212238e
EMS
25
26# Fonctionnalités communes à tous les query sets
d15017b2 27
15261361
EMS
28class RandomQuerySetMixin(object):
29 """Mixin pour les modèles.
30
31 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
32 méthode pour récupérer des objets au hasard.
33 """
34
35 def random(self, n=1):
36 """Récupère aléatoirement un nombre donné d'objets."""
bae03b7b
EMS
37 count = self.count()
38 positions = random.sample(xrange(count), min(n, count))
39 return [self[p] for p in positions]
15261361 40
5212238e
EMS
41class SEPQuerySet(models.query.QuerySet, RandomQuerySetMixin):
42 pass
43
44class SEPSphinxQuerySet(SphinxQuerySet, RandomQuerySetMixin):
45 """Fonctionnalités communes aux query sets de Sphinx."""
46
47 def __init__(self, model=None, index=None, weights=None):
48 SphinxQuerySet.__init__(self, model=model, index=index,
49 mode='SPH_MATCH_EXTENDED2',
50 rankmode='SPH_RANK_PROXIMITY_BM25',
51 weights=weights)
52
53 def add_to_query(self, query):
54 """Ajoute une partie à la requête texte."""
55 new_query = smart_unicode(self._query) + ' ' + query if self._query else query
56 return self.query(new_query)
57
58 def search(self, text):
59 """Recherche ``text`` dans tous les champs."""
60 return self.add_to_query('@* ' + text)
61
62 def filter_discipline(self, discipline):
63 """Par défaut, le filtre par discipline cherche le nom de la
64 discipline dans tous les champs."""
65 return self.search('"%s"' % discipline.nom)
66
67 def filter_region(self, region):
68 """Par défaut, le filtre par région cherche le nom de la région dans
69 tous les champs."""
70 return self.search('"%s"' % region.nom)
71
72class SEPManager(models.Manager):
73 """Lorsque les méthodes ``search``, ``filter_region`` et
74 ``filter_discipline`` sont appelées sur ce manager, le query set
75 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
76
77 def query(self, query):
78 return self.get_sphinx_query_set().query(query)
79
80 def add_to_query(self, query):
81 return self.get_sphinx_query_set().add_to_query(query)
82
83 def search(self, text):
84 return self.get_sphinx_query_set().search(text)
85
86 def filter_region(self, region):
87 return self.get_sphinx_query_set().filter_region(region)
88
89 def filter_discipline(self, discipline):
90 return self.get_sphinx_query_set().filter_discipline(discipline)
91
92# Disciplines
93
d15017b2
CR
94class Discipline(models.Model):
95 id = models.IntegerField(primary_key=True, db_column='id_discipline')
96 nom = models.CharField(max_length=765, db_column='nom_discipline')
6ef8ead4
CR
97
98 def __unicode__ (self):
92c7413b 99 return self.nom
6ef8ead4 100
d15017b2
CR
101 class Meta:
102 db_table = u'discipline'
103 ordering = ["nom",]
104
5212238e
EMS
105# Actualités
106
79c398f6
CR
107class SourceActualite(models.Model):
108 nom = models.CharField(max_length=255)
109 url = models.CharField(max_length=255)
ccbc4363 110
111 def __unicode__(self,):
112 return u"%s" % self.nom
79c398f6 113
5212238e 114class ActualiteQuerySet(SEPQuerySet):
2f9c4d6c 115
5212238e
EMS
116 def filter_date(self, min=None, max=None):
117 qs = self
118 if min:
119 qs = qs.filter(date__gte=min)
120 if max:
121 qs = qs.filter(date__lte=max)
122 return qs
da44ce68 123
5212238e 124class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
c1b134f8 125
5212238e
EMS
126 def __init__(self, model=None):
127 SEPSphinxQuerySet.__init__(self, model=model, index='actualites',
128 weights=dict(titre=3))
c1b134f8 129
5212238e
EMS
130 def filter_date(self, min=None, max=None):
131 qs = self
132 if min:
133 qs = qs.filter(date__gte=min.toordinal()+365)
134 if max:
135 qs = qs.filter(date__lte=max.toordinal()+365)
136 return qs
2f9c4d6c 137
5212238e
EMS
138class ActualiteManager(SEPManager):
139
140 def get_query_set(self):
141 return ActualiteQuerySet(self.model).filter(visible=True)
2f9c4d6c 142
5212238e
EMS
143 def get_sphinx_query_set(self):
144 return ActualiteSphinxQuerySet(self.model).order_by('-date')
bae03b7b 145
5212238e
EMS
146 def filter_date(self, min=None, max=None):
147 return self.get_query_set().filter_date(min=min, max=max)
bae03b7b 148
d15017b2 149class Actualite(models.Model):
4f262f90 150 id = models.AutoField(primary_key=True, db_column='id_actualite')
d15017b2
CR
151 titre = models.CharField(max_length=765, db_column='titre_actualite')
152 texte = models.TextField(db_column='texte_actualite')
153 url = models.CharField(max_length=765, db_column='url_actualite')
d15017b2 154 date = models.DateField(db_column='date_actualite')
f554ef70 155 visible = models.BooleanField(db_column='visible_actualite', default = False)
3b6456b0 156 ancienid = models.IntegerField(db_column='ancienId_actualite', blank = True, null = True)
ccbc4363 157 source = models.ForeignKey(SourceActualite, blank = True, null = True)
3a45eb64 158 disciplines = models.ManyToManyField(Discipline, blank=True, related_name="actualites")
a5f76eb4 159 regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
6ef8ead4 160
2f9c4d6c 161 objects = ActualiteManager()
5212238e 162 all_objects = models.Manager()
2f9c4d6c 163
d15017b2
CR
164 class Meta:
165 db_table = u'actualite'
166 ordering = ["-date",]
92c7413b 167
264a3210
EMS
168 def __unicode__ (self):
169 return "%s" % (self.titre)
170
171 def assigner_disciplines(self, disciplines):
172 self.disciplines.add(*disciplines)
173
174 def assigner_regions(self, regions):
175 self.regions.add(*regions)
176
5212238e 177# Agenda
4101cfc0 178
5212238e 179class EvenementQuerySet(SEPQuerySet):
4101cfc0 180
5212238e
EMS
181 def filter_type(self, type):
182 return self.filter(type=type)
4101cfc0 183
5212238e
EMS
184 def filter_debut(self, min=None, max=None):
185 qs = self
186 if min:
187 qs = qs.filter(debut__gte=min)
188 if max:
189 qs = qs.filter(debut__lt=max+datetime.timedelta(days=1))
190 return qs
c1b134f8 191
5212238e 192class EvenementSphinxQuerySet(SEPSphinxQuerySet):
c1b134f8 193
5212238e
EMS
194 def __init__(self, model=None):
195 SEPSphinxQuerySet.__init__(self, model=model, index='evenements',
196 weights=dict(titre=3))
4101cfc0 197
5212238e
EMS
198 def filter_type(self, type):
199 return self.add_to_query('@type "%s"' % type)
200
201 def filter_debut(self, min=None, max=None):
7bbf600c 202 qs = self
5212238e
EMS
203 if min:
204 qs = qs.filter(debut__gte=min.toordinal()+365)
205 if max:
206 qs = qs.filter(debut__lte=max.toordinal()+365)
7bbf600c
EMS
207 return qs
208
5212238e 209class EvenementManager(SEPManager):
bae03b7b 210
5212238e
EMS
211 def get_query_set(self):
212 return EvenementQuerySet(self.model).filter(approuve=True)
213
214 def get_sphinx_query_set(self):
215 return EvenementSphinxQuerySet(self.model).order_by('-debut')
216
217 def filter_type(self, type):
218 return self.get_query_set().filter_type(type)
219
220 def filter_debut(self, min=None, max=None):
221 return self.get_query_set().filter_debut(min=min, max=max)
bae03b7b 222
86983865
EMS
223def build_time_zone_choices():
224 fr_names = set()
225 tzones = []
226 now = datetime.datetime.now()
227 for tzname in pytz.common_timezones:
228 tz = pytz.timezone(tzname)
229 fr_name = get_timezone_name(tz, locale='fr_FR')
230 if fr_name in fr_names:
231 continue
232 fr_names.add(fr_name)
233 offset = tz.utcoffset(now)
234 seconds = offset.seconds + offset.days * 86400
235 (hours, minutes) = divmod(seconds // 60, 60)
236 offset_str = 'UTC%+d:%d' % (hours, minutes) if minutes else 'UTC%+d' % hours
237 tzones.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
238 tzones.sort()
239 return [(tz[1], tz[2]) for tz in tzones]
240
92c7413b 241class Evenement(models.Model):
7bbf600c
EMS
242 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
243 (u'Conférence', u'Conférence'),
244 (u'Appel à contribution', u'Appel à contribution'),
245 (u'Journée d\'étude', u'Journée d\'étude'),
5212238e 246 (u'None', u'Autre'))
86983865
EMS
247 TIME_ZONE_CHOICES = build_time_zone_choices()
248
74b087e5 249 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
a5f76eb4 250 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
92c7413b
CR
251 titre = models.CharField(max_length=255)
252 discipline = models.ForeignKey('Discipline', related_name = "discipline",
253 blank = True, null = True)
a5f76eb4
EMS
254 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
255 verbose_name=u"discipline secondaire",
256 blank=True, null=True)
74b087e5 257 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
7bbf600c 258 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
86983865 259 lieu = models.TextField()
74b087e5
EMS
260 debut = models.DateTimeField(default=datetime.datetime.now)
261 fin = models.DateTimeField(default=datetime.datetime.now)
86983865 262 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
74b087e5
EMS
263 description = models.TextField(blank=True, null=True)
264 contact = models.TextField(blank=True, null=True)
265 url = models.CharField(max_length=255, blank=True, null=True)
266 piece_jointe = models.FileField(upload_to='agenda/pj', blank=True, verbose_name='pièce jointe')
a5f76eb4 267 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
92c7413b 268
4101cfc0 269 objects = EvenementManager()
5212238e 270 all_objects = models.Manager()
4101cfc0
EMS
271
272 class Meta:
273 ordering = ['-debut']
274
020f79a9 275 def __unicode__(self,):
276 return "[%s] %s" % (self.uid, self.titre)
277
27fe0d70
EMS
278 def piece_jointe_display(self):
279 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
280
73309469 281 def clean(self):
282 from django.core.exceptions import ValidationError
283 if self.debut > self.fin:
284 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
285
b7a741ad 286 def save(self, *args, **kwargs):
287 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
288 approuvé"""
73309469 289 self.clean()
b7a741ad 290 self.update_vevent()
291 super(Evenement, self).save(*args, **kwargs)
292
293 # methodes de commnunications avec CALDAV
294 def as_ical(self,):
295 """Retourne l'evenement django sous forme d'objet icalendar"""
296 cal = vobject.iCalendar()
297 cal.add('vevent')
298
299 # fournit son propre uid
7f56d0d4 300 if self.uid in [None, ""]:
b7a741ad 301 self.uid = str(uuid.uuid1())
302
303 cal.vevent.add('uid').value = self.uid
304
305 cal.vevent.add('summary').value = self.titre
306
307 if self.mots_cles is None:
308 kw = []
309 else:
310 kw = self.mots_cles.split(",")
311
312 try:
313 kw.append(self.discipline.nom)
314 kw.append(self.discipline_secondaire.nom)
315 kw.append(self.type)
316 except: pass
317
79b400f0 318 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
b7a741ad 319 for k in kw:
320 cal.vevent.add('x-auf-keywords').value = k
321
322 description = self.description
323 if len(kw) > 0:
324 if len(self.description) > 0:
325 description += "\n"
028f548f 326 description += u"Mots-clés: " + ", ".join(kw)
b7a741ad 327
7f214e0f
EMS
328 cal.vevent.add('dtstart').value = combine(self.debut, pytz.timezone(self.fuseau))
329 cal.vevent.add('dtend').value = combine(self.fin, pytz.timezone(self.fuseau))
b7a741ad 330 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
331 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
79b400f0 332 if len(description) > 0:
b7a741ad 333 cal.vevent.add('description').value = description
334 if len(self.contact) > 0:
335 cal.vevent.add('contact').value = self.contact
336 if len(self.url) > 0:
337 cal.vevent.add('url').value = self.url
338 if len(self.lieu) > 0:
339 cal.vevent.add('location').value = self.lieu
74b087e5
EMS
340 if self.piece_jointe:
341 url = self.piece_jointe.url
342 if not url.startswith('http://'):
343 url = SITE_ROOT_URL + url
344 cal.vevent.add('attach').value = url
b7a741ad 345 return cal
346
347 def update_vevent(self,):
348 """Essaie de créer l'évènement sur le serveur ical.
349 En cas de succès, l'évènement local devient donc inactif et approuvé"""
350 try:
351 if self.approuve:
352 event = self.as_ical()
353 client = caldav.DAVClient(CALENDRIER_URL)
354 cal = caldav.Calendar(client, url = CALENDRIER_URL)
355 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
356 e.save()
357 except:
358 self.approuve = False
359
360 def delete_vevent(self,):
361 """Supprime l'evenement sur le serveur caldav"""
362 try:
363 if self.approuve:
364 event = self.as_ical()
365 client = caldav.DAVClient(CALENDRIER_URL)
366 cal = caldav.Calendar(client, url = CALENDRIER_URL)
367 e = cal.event(self.uid)
368 e.delete()
369 except error.NotFoundError:
370 pass
371
264a3210
EMS
372 def assigner_regions(self, regions):
373 self.regions.add(*regions)
374
375 def assigner_disciplines(self, disciplines):
376 if len(disciplines) == 1:
377 if self.discipline:
378 self.discipline_secondaire = disciplines[0]
379 else:
380 self.discipline = disciplines[0]
381 elif len(disciplines) >= 2:
382 self.discipline = disciplines[0]
383 self.discipline_secondaire = disciplines[1]
384
b7a741ad 385def delete_vevent(sender, instance, *args, **kwargs):
5212238e
EMS
386 # Surcharge du comportement de suppression
387 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
388 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
b7a741ad 389 instance.delete_vevent()
5212238e 390pre_delete.connect(delete_vevent, sender=Evenement)
b7a741ad 391
5212238e 392# Ressources
b7a741ad 393
d972b61d 394class ListSet(models.Model):
395 spec = models.CharField(primary_key = True, max_length = 255)
396 name = models.CharField(max_length = 255)
397 server = models.CharField(max_length = 255)
9eda5d6c 398 validated = models.BooleanField(default = True)
d972b61d 399
10d37e44 400 def __unicode__(self,):
401 return self.name
402
5212238e 403class RecordSphinxQuerySet(SEPSphinxQuerySet):
f153be1b 404
5212238e
EMS
405 def __init__(self, model=None):
406 SEPSphinxQuerySet.__init__(self, model=model, index='ressources',
407 weights=dict(title=3))
c1b134f8 408
5212238e 409class RecordManager(SEPManager):
f12cc7fb 410
5212238e 411 def get_query_set(self):
f153be1b
EMS
412 """Ne garder que les ressources validées et qui sont soit dans aucun
413 listset ou au moins dans un listset validé."""
5212238e
EMS
414 qs = SEPQuerySet(self.model)
415 qs = qs.filter(validated=True)
82f25472
EMS
416 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
417 return qs.distinct()
f153be1b 418
5212238e
EMS
419 def get_sphinx_query_set(self):
420 return RecordSphinxQuerySet(self.model)
77b0fac0 421
0cc5f772 422class Record(models.Model):
23b5b3d5 423
424 #fonctionnement interne
0cc5f772 425 id = models.AutoField(primary_key = True)
a5f76eb4 426 server = models.CharField(max_length = 255, verbose_name=u'serveur')
23b5b3d5 427 last_update = models.CharField(max_length = 255)
428 last_checksum = models.CharField(max_length = 255)
a5f76eb4 429 validated = models.BooleanField(default=True, verbose_name=u'validé')
23b5b3d5 430
431 #OAI
18dbd2cf
EMS
432 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
433 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
434 description = models.TextField(null=True, blank=True)
435 modified = models.CharField(max_length=255, null=True, blank=True)
23b5b3d5 436 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
437 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
438 source = models.TextField(null = True, blank = True)
439 contributor = models.TextField(null = True, blank = True)
18dbd2cf 440 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
23b5b3d5 441 publisher = models.TextField(null = True, blank = True)
442 type = models.TextField(null = True, blank = True)
443 format = models.TextField(null = True, blank = True)
444 language = models.TextField(null = True, blank = True)
da9020f3 445
c88d78dc 446 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
d972b61d 447
da9020f3 448 #SEP 2 (aucune données récoltées)
23b5b3d5 449 alt_title = models.TextField(null = True, blank = True)
450 abstract = models.TextField(null = True, blank = True)
451 creation = models.CharField(max_length = 255, null = True, blank = True)
452 issued = models.CharField(max_length = 255, null = True, blank = True)
453 isbn = models.TextField(null = True, blank = True)
454 orig_lang = models.TextField(null = True, blank = True)
da9020f3 455
456 # Metadata AUF multivaluées
a342f93a
EMS
457 disciplines = models.ManyToManyField(Discipline, blank=True)
458 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
459 pays = models.ManyToManyField(Pays, blank=True)
460 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
0cc5f772 461
5212238e 462 # Managers
da44ce68 463 objects = RecordManager()
5212238e 464 all_objects = models.Manager()
da44ce68 465
18dbd2cf
EMS
466 class Meta:
467 verbose_name = 'ressource'
468
264a3210
EMS
469 def __unicode__(self):
470 return "[%s] %s" % (self.server, self.title)
471
472 def getServeurURL(self):
f98ad449 473 """Retourne l'URL du serveur de provenance"""
474 return RESOURCES[self.server]['url']
475
264a3210 476 def est_complet(self):
6d885e0c 477 """teste si le record à toutes les données obligatoires"""
478 return self.disciplines.count() > 0 and \
479 self.thematiques.count() > 0 and \
480 self.pays.count() > 0 and \
481 self.regions.count() > 0
482
264a3210
EMS
483 def assigner_regions(self, regions):
484 self.regions.add(*regions)
da9020f3 485
264a3210
EMS
486 def assigner_disciplines(self, disciplines):
487 self.disciplines.add(*disciplines)
264a3210 488
6d885e0c 489class Serveur(models.Model):
b7a741ad 490 """Identification d'un serveur d'ou proviennent les références"""
6d885e0c 491 nom = models.CharField(primary_key = True, max_length = 255)
492
493 def __unicode__(self,):
494 return self.nom
495
496 def conf_2_db(self,):
497 for k in RESOURCES.keys():
498 s, created = Serveur.objects.get_or_create(nom=k)
499 s.nom = k
500 s.save()
501
502class Profile(models.Model):
503 user = models.ForeignKey(User, unique=True)
504 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
0cc5f772
CR
505
506class HarvestLog(models.Model):
23b5b3d5 507 context = models.CharField(max_length = 255)
508 name = models.CharField(max_length = 255)
0cc5f772 509 date = models.DateTimeField(auto_now = True)
23b5b3d5 510 added = models.IntegerField(null = True, blank = True)
511 updated = models.IntegerField(null = True, blank = True)
a85ba76e 512 processed = models.IntegerField(null = True, blank = True)
23b5b3d5 513 record = models.ForeignKey(Record, null = True, blank = True)
514
515 @staticmethod
516 def add(message):
517 logger = HarvestLog()
518 if message.has_key('record_id'):
519 message['record'] = Record.objects.get(id=message['record_id'])
520 del(message['record_id'])
521
522 for k,v in message.items():
523 setattr(logger, k, v)
524 logger.save()