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