Fils RSS pour toutes les briques
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
1 # -*- encoding: utf-8 -*-
2 import caldav
3 import datetime
4 import feedparser
5 import operator
6 import os
7 import pytz
8 import random
9 import uuid
10 import vobject
11 from urllib import urlencode
12
13 from backend_config import RESOURCES
14 from babel.dates import get_timezone_name
15 from caldav.lib import error
16 from django.contrib.auth.models import User
17 from django.contrib.contenttypes.models import ContentType
18 from django.core.urlresolvers import reverse
19 from django.db import models
20 from django.db.models import Q, Max
21 from django.db.models.signals import pre_delete
22 from django.utils.encoding import smart_unicode
23 from djangosphinx.models import SphinxQuerySet, SearchError
24
25 from datamaster_modeles.models import Region, Pays, Thematique
26 from savoirs.globals import META
27 from settings import CALENDRIER_URL, SITE_ROOT_URL
28
29 # Fonctionnalités communes à tous les query sets
30
31 class RandomQuerySetMixin(object):
32 """Mixin pour les modèles.
33
34 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
35 méthode pour récupérer des objets au hasard.
36 """
37
38 def random(self, n=1):
39 """Récupère aléatoirement un nombre donné d'objets."""
40 count = self.count()
41 positions = random.sample(xrange(count), min(n, count))
42 return [self[p] for p in positions]
43
44 class SEPQuerySet(models.query.QuerySet, RandomQuerySetMixin):
45
46 def _filter_date(self, field, min=None, max=None):
47 """Limite les résultats à ceux dont le champ ``field`` tombe entre
48 les dates ``min`` et ``max``."""
49 qs = self
50 if min:
51 qs = qs.filter(**{field + '__gte': min})
52 if max:
53 qs = qs.filter(**{field + '__lte': max})
54 return qs
55
56 class SEPSphinxQuerySet(SphinxQuerySet, RandomQuerySetMixin):
57 """Fonctionnalités communes aux query sets de Sphinx."""
58
59 def __init__(self, model=None, index=None, weights=None):
60 SphinxQuerySet.__init__(self, model=model, index=index,
61 mode='SPH_MATCH_EXTENDED2',
62 rankmode='SPH_RANK_PROXIMITY_BM25',
63 weights=weights)
64
65 def add_to_query(self, query):
66 """Ajoute une partie à la requête texte."""
67
68 # Assurons-nous qu'il y a un nombre pair de guillemets
69 if query.count('"') % 2 != 0:
70 # Sinon, on enlève le dernier (faut choisir...)
71 i = query.rindex('"')
72 query = query[:i] + query[i+1:]
73
74 new_query = smart_unicode(self._query) + ' ' + query if self._query else query
75 return self.query(new_query)
76
77 def search(self, text):
78 """Recherche ``text`` dans tous les champs."""
79 return self.add_to_query('@* ' + text)
80
81 def filter_discipline(self, discipline):
82 """Par défaut, le filtre par discipline cherche le nom de la
83 discipline dans tous les champs."""
84 return self.search('"%s"' % discipline.nom)
85
86 def filter_region(self, region):
87 """Par défaut, le filtre par région cherche le nom de la région dans
88 tous les champs."""
89 return self.search('"%s"' % region.nom)
90
91 def _filter_date(self, field, min=None, max=None):
92 """Limite les résultats à ceux dont le champ ``field`` tombe entre
93 les dates ``min`` et ``max``."""
94 qs = self
95 if min:
96 qs = qs.filter(**{field + '__gte': min.toordinal()+365})
97 if max:
98 qs = qs.filter(**{field + '__lte': max.toordinal()+365})
99 return qs
100
101 def _get_sphinx_results(self):
102 try:
103 return SphinxQuerySet._get_sphinx_results(self)
104 except SearchError:
105 # Essayons d'enlever les caractères qui peuvent poser problème.
106 for c in '|!@()~/<=^$':
107 self._query = self._query.replace(c, ' ')
108 try:
109 return SphinxQuerySet._get_sphinx_results(self)
110 except SearchError:
111 # Ça ne marche toujours pas. Enlevons les guillemets et les
112 # tirets.
113 for c in '"-':
114 self._query = self._query.replace(c, ' ')
115 return SphinxQuerySet._get_sphinx_results(self)
116
117 class SEPManager(models.Manager):
118 """Lorsque les méthodes ``search``, ``filter_region`` et
119 ``filter_discipline`` sont appelées sur ce manager, le query set
120 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
121
122 def query(self, query):
123 return self.get_sphinx_query_set().query(query)
124
125 def add_to_query(self, query):
126 return self.get_sphinx_query_set().add_to_query(query)
127
128 def search(self, text):
129 return self.get_sphinx_query_set().search(text)
130
131 def filter_region(self, region):
132 return self.get_sphinx_query_set().filter_region(region)
133
134 def filter_discipline(self, discipline):
135 return self.get_sphinx_query_set().filter_discipline(discipline)
136
137 # Disciplines
138
139 class Discipline(models.Model):
140 id = models.IntegerField(primary_key=True, db_column='id_discipline')
141 nom = models.CharField(max_length=765, db_column='nom_discipline')
142
143 def __unicode__ (self):
144 return self.nom
145
146 class Meta:
147 db_table = u'discipline'
148 ordering = ["nom",]
149
150 # Actualités
151
152 class SourceActualite(models.Model):
153 TYPE_CHOICES = (
154 ('actu', 'Actualités'),
155 ('appels', "Appels d'offres"),
156 )
157
158 nom = models.CharField(max_length=255)
159 url = models.CharField(max_length=255, verbose_name='URL', blank=True)
160 type = models.CharField(max_length=10, default='actu', choices=TYPE_CHOICES)
161
162 class Meta:
163 verbose_name = u'fil RSS syndiqué'
164 verbose_name_plural = u'fils RSS syndiqués'
165
166 def __unicode__(self,):
167 return u"%s (%s)" % (self.nom, self.get_type_display())
168
169 def update(self):
170 """Mise à jour du fil RSS."""
171 if not self.url:
172 return
173 feed = feedparser.parse(self.url)
174 for entry in feed.entries:
175 if Actualite.all_objects.filter(url=entry.link).count() == 0:
176 ts = entry.updated_parsed
177 date = datetime.date(ts.tm_year, ts.tm_mon, ts.tm_mday)
178 a = self.actualites.create(titre=entry.title,
179 texte=entry.summary_detail.value,
180 url=entry.link, date=date)
181
182 class ActualiteQuerySet(SEPQuerySet):
183
184 def filter_date(self, min=None, max=None):
185 return self._filter_date('date', min=min, max=max)
186
187 def filter_type(self, type):
188 return self.filter(source__type=type)
189
190 class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
191
192 def __init__(self, model=None):
193 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_actualites',
194 weights=dict(titre=3))
195
196 def filter_date(self, min=None, max=None):
197 return self._filter_date('date', min=min, max=max)
198
199 TYPE_CODES = {'actu': 1, 'appels': 2}
200 def filter_type(self, type):
201 return self.filter(type=self.TYPE_CODES[type])
202
203 class ActualiteManager(SEPManager):
204
205 def get_query_set(self):
206 return ActualiteQuerySet(self.model).filter(visible=True)
207
208 def get_sphinx_query_set(self):
209 return ActualiteSphinxQuerySet(self.model).order_by('-date')
210
211 def filter_date(self, min=None, max=None):
212 return self.get_query_set().filter_date(min=min, max=max)
213
214 def filter_type(self, type):
215 return self.get_query_set().filter_type(type)
216
217 class Actualite(models.Model):
218 id = models.AutoField(primary_key=True, db_column='id_actualite')
219 titre = models.CharField(max_length=765, db_column='titre_actualite')
220 texte = models.TextField(db_column='texte_actualite')
221 url = models.CharField(max_length=765, db_column='url_actualite')
222 date = models.DateField(db_column='date_actualite')
223 visible = models.BooleanField(db_column='visible_actualite', default=False)
224 ancienid = models.IntegerField(db_column='ancienId_actualite', blank=True, null=True)
225 source = models.ForeignKey(SourceActualite, related_name='actualites')
226 disciplines = models.ManyToManyField(Discipline, blank=True, related_name="actualites")
227 regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
228
229 objects = ActualiteManager()
230 all_objects = models.Manager()
231
232 class Meta:
233 db_table = u'actualite'
234 ordering = ["-date"]
235
236 def __unicode__ (self):
237 return "%s" % (self.titre)
238
239 def get_absolute_url(self):
240 return reverse('actualite', kwargs={'id': self.id})
241
242 def assigner_disciplines(self, disciplines):
243 self.disciplines.add(*disciplines)
244
245 def assigner_regions(self, regions):
246 self.regions.add(*regions)
247
248 # Agenda
249
250 class EvenementQuerySet(SEPQuerySet):
251
252 def filter_type(self, type):
253 return self.filter(type=type)
254
255 def filter_debut(self, min=None, max=None):
256 qs = self
257 if min:
258 qs = qs.filter(debut__gte=min)
259 if max:
260 qs = qs.filter(debut__lt=max+datetime.timedelta(days=1))
261 return qs
262
263 class EvenementSphinxQuerySet(SEPSphinxQuerySet):
264
265 def __init__(self, model=None):
266 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_evenements',
267 weights=dict(titre=3))
268
269 def filter_type(self, type):
270 return self.add_to_query('@type "%s"' % type)
271
272 def filter_debut(self, min=None, max=None):
273 return self._filter_date('debut', min=min, max=max)
274
275 class EvenementManager(SEPManager):
276
277 def get_query_set(self):
278 return EvenementQuerySet(self.model).filter(approuve=True)
279
280 def get_sphinx_query_set(self):
281 return EvenementSphinxQuerySet(self.model).order_by('-debut')
282
283 def filter_type(self, type):
284 return self.get_query_set().filter_type(type)
285
286 def filter_debut(self, min=None, max=None):
287 return self.get_query_set().filter_debut(min=min, max=max)
288
289 def build_time_zone_choices(pays=None):
290 timezones = pytz.country_timezones[pays] if pays else pytz.common_timezones
291 result = []
292 now = datetime.datetime.now()
293 for tzname in timezones:
294 tz = pytz.timezone(tzname)
295 fr_name = get_timezone_name(tz, locale='fr_FR')
296 offset = tz.utcoffset(now)
297 seconds = offset.seconds + offset.days * 86400
298 (hours, minutes) = divmod(seconds // 60, 60)
299 offset_str = 'UTC%+d:%d' % (hours, minutes) if minutes else 'UTC%+d' % hours
300 result.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
301 result.sort()
302 return [(x[1], x[2]) for x in result]
303
304 class Evenement(models.Model):
305 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
306 (u'Conférence', u'Conférence'),
307 (u'Appel à contribution', u'Appel à contribution'),
308 (u'Journée d\'étude', u'Journée d\'étude'),
309 (u'Autre', u'Autre'))
310 TIME_ZONE_CHOICES = build_time_zone_choices()
311
312 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
313 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
314 titre = models.CharField(max_length=255)
315 discipline = models.ForeignKey('Discipline', related_name = "discipline",
316 blank = True, null = True)
317 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
318 verbose_name=u"discipline secondaire",
319 blank=True, null=True)
320 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
321 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
322 adresse = models.TextField()
323 ville = models.CharField(max_length=100)
324 pays = models.ForeignKey(Pays, null=True, related_name='evenements')
325 debut = models.DateTimeField(default=datetime.datetime.now)
326 fin = models.DateTimeField(default=datetime.datetime.now)
327 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
328 description = models.TextField()
329 contact = models.TextField(null=True) # champ obsolète
330 prenom = models.CharField('prénom', max_length=100)
331 nom = models.CharField(max_length=100)
332 courriel = models.EmailField()
333 url = models.CharField(max_length=255, blank=True, null=True)
334 piece_jointe = models.FileField(upload_to='agenda/pj', blank=True, verbose_name='pièce jointe')
335 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions additionnelles',
336 help_text="On considère d'emblée que l'évènement se déroule dans la région "
337 "dans laquelle se trouve le pays indiqué plus haut. Il est possible "
338 "de désigner ici des régions additionnelles.")
339
340 objects = EvenementManager()
341 all_objects = models.Manager()
342
343 class Meta:
344 ordering = ['-debut']
345 verbose_name = u'évènement'
346 verbose_name_plural = u'évènements'
347
348 def __unicode__(self):
349 return "[%s] %s" % (self.uid, self.titre)
350
351 def get_absolute_url(self):
352 return reverse('evenement', kwargs={'id': self.id})
353
354 def duration_display(self):
355 delta = self.fin - self.debut
356 minutes, seconds = divmod(delta.seconds, 60)
357 hours, minutes = divmod(minutes, 60)
358 days = delta.days
359 parts = []
360 if days == 1:
361 parts.append('1 jour')
362 elif days > 1:
363 parts.append('%d jours' % days)
364 if hours == 1:
365 parts.append('1 heure')
366 elif hours > 1:
367 parts.append('%d heures' % hours)
368 if minutes == 1:
369 parts.append('1 minute')
370 elif minutes > 1:
371 parts.append('%d minutes' % minutes)
372 return ' '.join(parts)
373
374 def piece_jointe_display(self):
375 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
376
377 def courriel_display(self):
378 return self.courriel.replace(u'@', u' (à) ')
379
380 def clean(self):
381 from django.core.exceptions import ValidationError
382 if self.debut > self.fin:
383 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
384
385 def save(self, *args, **kwargs):
386 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
387 approuvé"""
388 self.contact = '' # Vider ce champ obsolète à la première occasion...
389 self.clean()
390 super(Evenement, self).save(*args, **kwargs)
391 self.update_vevent()
392
393 # methodes de commnunications avec CALDAV
394 def as_ical(self,):
395 """Retourne l'evenement django sous forme d'objet icalendar"""
396 cal = vobject.iCalendar()
397 cal.add('vevent')
398
399 # fournit son propre uid
400 if self.uid in [None, ""]:
401 self.uid = str(uuid.uuid1())
402
403 cal.vevent.add('uid').value = self.uid
404
405 cal.vevent.add('summary').value = self.titre
406
407 if self.mots_cles is None:
408 kw = []
409 else:
410 kw = self.mots_cles.split(",")
411
412 try:
413 kw.append(self.discipline.nom)
414 kw.append(self.discipline_secondaire.nom)
415 kw.append(self.type)
416 except: pass
417
418 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
419 for k in kw:
420 cal.vevent.add('x-auf-keywords').value = k
421
422 description = self.description
423 if len(kw) > 0:
424 if len(self.description) > 0:
425 description += "\n"
426 description += u"Mots-clés: " + ", ".join(kw)
427
428 cal.vevent.add('dtstart').value = combine(self.debut, pytz.timezone(self.fuseau))
429 cal.vevent.add('dtend').value = combine(self.fin, pytz.timezone(self.fuseau))
430 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
431 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
432 if len(description) > 0:
433 cal.vevent.add('description').value = description
434 if len(self.contact) > 0:
435 cal.vevent.add('contact').value = self.contact
436 if len(self.url) > 0:
437 cal.vevent.add('url').value = self.url
438 cal.vevent.add('location').value = ', '.join([x for x in [self.adresse, self.ville, self.pays.nom] if x])
439 if self.piece_jointe:
440 url = self.piece_jointe.url
441 if not url.startswith('http://'):
442 url = SITE_ROOT_URL + url
443 cal.vevent.add('attach').value = url
444 return cal
445
446 def update_vevent(self,):
447 """Essaie de créer l'évènement sur le serveur ical.
448 En cas de succès, l'évènement local devient donc inactif et approuvé"""
449 try:
450 if self.approuve:
451 event = self.as_ical()
452 client = caldav.DAVClient(CALENDRIER_URL)
453 cal = caldav.Calendar(client, url = CALENDRIER_URL)
454 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
455 e.save()
456 except:
457 self.approuve = False
458
459 def delete_vevent(self,):
460 """Supprime l'evenement sur le serveur caldav"""
461 try:
462 if self.approuve:
463 event = self.as_ical()
464 client = caldav.DAVClient(CALENDRIER_URL)
465 cal = caldav.Calendar(client, url = CALENDRIER_URL)
466 e = cal.event(self.uid)
467 e.delete()
468 except error.NotFoundError:
469 pass
470
471 def assigner_regions(self, regions):
472 self.regions.add(*regions)
473
474 def assigner_disciplines(self, disciplines):
475 if len(disciplines) == 1:
476 if self.discipline:
477 self.discipline_secondaire = disciplines[0]
478 else:
479 self.discipline = disciplines[0]
480 elif len(disciplines) >= 2:
481 self.discipline = disciplines[0]
482 self.discipline_secondaire = disciplines[1]
483
484 def delete_vevent(sender, instance, *args, **kwargs):
485 # Surcharge du comportement de suppression
486 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
487 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
488 instance.delete_vevent()
489 pre_delete.connect(delete_vevent, sender=Evenement)
490
491 # Ressources
492
493 class ListSet(models.Model):
494 spec = models.CharField(primary_key = True, max_length = 255)
495 name = models.CharField(max_length = 255)
496 server = models.CharField(max_length = 255)
497 validated = models.BooleanField(default = True)
498
499 def __unicode__(self,):
500 return self.name
501
502 class RecordQuerySet(SEPQuerySet):
503
504 def filter_modified(self, min=None, max=None):
505 return self._filter_date(self, 'modified', min=min, max=max)
506
507 class RecordSphinxQuerySet(SEPSphinxQuerySet):
508
509 def __init__(self, model=None):
510 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_ressources',
511 weights=dict(title=3))
512
513 def filter_modified(self, min=None, max=None):
514 return self._filter_date(self, 'modified', min=min, max=max)
515
516 class RecordManager(SEPManager):
517
518 def get_query_set(self):
519 """Ne garder que les ressources validées et qui sont soit dans aucun
520 listset ou au moins dans un listset validé."""
521 qs = RecordQuerySet(self.model)
522 qs = qs.filter(validated=True)
523 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
524 return qs.distinct()
525
526 def get_sphinx_query_set(self):
527 return RecordSphinxQuerySet(self.model)
528
529 def filter_modified(self, min=None, max=None):
530 return self.get_query_set().filter_modified(min=min, max=max)
531
532 class Record(models.Model):
533
534 #fonctionnement interne
535 id = models.AutoField(primary_key = True)
536 server = models.CharField(max_length = 255, verbose_name=u'serveur')
537 last_update = models.CharField(max_length = 255)
538 last_checksum = models.CharField(max_length = 255)
539 validated = models.BooleanField(default=True, verbose_name=u'validé')
540
541 #OAI
542 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
543 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
544 description = models.TextField(null=True, blank=True)
545 modified = models.CharField(max_length=255, null=True, blank=True)
546 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
547 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
548 source = models.TextField(null = True, blank = True)
549 contributor = models.TextField(null = True, blank = True)
550 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
551 publisher = models.TextField(null = True, blank = True)
552 type = models.TextField(null = True, blank = True)
553 format = models.TextField(null = True, blank = True)
554 language = models.TextField(null = True, blank = True)
555
556 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
557
558 #SEP 2 (aucune données récoltées)
559 alt_title = models.TextField(null = True, blank = True)
560 abstract = models.TextField(null = True, blank = True)
561 creation = models.CharField(max_length = 255, null = True, blank = True)
562 issued = models.CharField(max_length = 255, null = True, blank = True)
563 isbn = models.TextField(null = True, blank = True)
564 orig_lang = models.TextField(null = True, blank = True)
565
566 # Metadata AUF multivaluées
567 disciplines = models.ManyToManyField(Discipline, blank=True)
568 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
569 pays = models.ManyToManyField(Pays, blank=True)
570 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
571
572 # Managers
573 objects = RecordManager()
574 all_objects = models.Manager()
575
576 class Meta:
577 verbose_name = 'ressource'
578
579 def __unicode__(self):
580 return "[%s] %s" % (self.server, self.title)
581
582 def get_absolute_url(self):
583 return reverse('ressource', kwargs={'id': self.id})
584
585 def getServeurURL(self):
586 """Retourne l'URL du serveur de provenance"""
587 return RESOURCES[self.server]['url']
588
589 def est_complet(self):
590 """teste si le record à toutes les données obligatoires"""
591 return self.disciplines.count() > 0 and \
592 self.thematiques.count() > 0 and \
593 self.pays.count() > 0 and \
594 self.regions.count() > 0
595
596 def assigner_regions(self, regions):
597 self.regions.add(*regions)
598
599 def assigner_disciplines(self, disciplines):
600 self.disciplines.add(*disciplines)
601
602 class Serveur(models.Model):
603 """Identification d'un serveur d'ou proviennent les références"""
604 nom = models.CharField(primary_key = True, max_length = 255)
605
606 def __unicode__(self,):
607 return self.nom
608
609 def conf_2_db(self,):
610 for k in RESOURCES.keys():
611 s, created = Serveur.objects.get_or_create(nom=k)
612 s.nom = k
613 s.save()
614
615 class Profile(models.Model):
616 user = models.ForeignKey(User, unique=True)
617 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
618
619 class HarvestLog(models.Model):
620 context = models.CharField(max_length = 255)
621 name = models.CharField(max_length = 255)
622 date = models.DateTimeField(auto_now = True)
623 added = models.IntegerField(null = True, blank = True)
624 updated = models.IntegerField(null = True, blank = True)
625 processed = models.IntegerField(null = True, blank = True)
626 record = models.ForeignKey(Record, null = True, blank = True)
627
628 @staticmethod
629 def add(message):
630 logger = HarvestLog()
631 if message.has_key('record_id'):
632 message['record'] = Record.all_objects.get(id=message['record_id'])
633 del(message['record_id'])
634
635 for k,v in message.items():
636 setattr(logger, k, v)
637 logger.save()
638
639 # Pages statiques
640
641 class PageStatique(models.Model):
642 id = models.CharField(max_length=32, primary_key=True)
643 titre = models.CharField(max_length=100)
644 contenu = models.TextField()
645
646 class Meta:
647 verbose_name_plural = 'pages statiques'
648
649 # Recherches
650
651 class GlobalSearchResults(object):
652
653 def __init__(self, actualites=None, appels=None, evenements=None,
654 ressources=None, chercheurs=None, sites=None, sites_auf=None):
655 self.actualites = actualites
656 self.appels = appels
657 self.evenements = evenements
658 self.ressources = ressources
659 self.chercheurs = chercheurs
660 self.sites = sites
661 self.sites_auf = sites_auf
662
663 class Search(models.Model):
664 user = models.ForeignKey(User, editable=False)
665 content_type = models.ForeignKey(ContentType, editable=False)
666 nom = models.CharField(max_length=100, verbose_name="nom de la recherche")
667 q = models.CharField(max_length=100, blank=True, verbose_name="rechercher dans tous les champs")
668 discipline = models.ForeignKey(Discipline, blank=True, null=True)
669 region = models.ForeignKey(Region, blank=True, null=True, verbose_name='région',
670 help_text="La région est ici définie au sens, non strictement géographique, du Bureau régional de l'AUF de référence.")
671
672 def query_string(self):
673 params = dict()
674 for field in self._meta.fields:
675 if field.name in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
676 continue
677 value = getattr(self, field.column)
678 if value:
679 if isinstance(value, datetime.date):
680 params[field.name] = value.strftime('%d/%m/%Y')
681 else:
682 params[field.name] = value
683 return urlencode(params)
684
685 class Meta:
686 verbose_name = 'recherche transversale'
687 verbose_name_plural = "recherches transversales"
688
689 def save(self):
690 if (not self.content_type_id):
691 self.content_type = ContentType.objects.get_for_model(self.__class__)
692 super(Search, self).save()
693
694 def as_leaf_class(self):
695 content_type = self.content_type
696 model = content_type.model_class()
697 if(model == Search):
698 return self
699 return model.objects.get(id=self.id)
700
701 def run(self):
702 from chercheurs.models import Chercheur
703 from sitotheque.models import Site
704
705 results = object()
706 actualites = Actualite.objects
707 evenements = Evenement.objects
708 ressources = Record.objects
709 chercheurs = Chercheur.objects
710 sites = Site.objects
711 if self.q:
712 actualites = actualites.search(self.q)
713 evenements = evenements.search(self.q)
714 ressources = ressources.search(self.q)
715 chercheurs = chercheurs.search(self.q)
716 sites = sites.search(self.q)
717 if self.discipline:
718 actualites = actualites.filter_discipline(self.discipline)
719 evenements = evenements.filter_discipline(self.discipline)
720 ressources = ressources.filter_discipline(self.discipline)
721 chercheurs = chercheurs.filter_discipline(self.discipline)
722 sites = sites.filter_discipline(self.discipline)
723 if self.region:
724 actualites = actualites.filter_region(self.region)
725 evenements = evenements.filter_region(self.region)
726 ressources = ressources.filter_region(self.region)
727 chercheurs = chercheurs.filter_region(self.region)
728 sites = sites.filter_region(self.region)
729 try:
730 sites_auf = google_search(0, self.q)['results']
731 except:
732 sites_auf = []
733
734 return GlobalSearchResults(
735 actualites=actualites.order_by('-date').filter_type('actu'),
736 appels=actualites.order_by('-date').filter_type('appels'),
737 evenements=evenements.order_by('-debut'),
738 ressources=ressources.order_by('-id'),
739 chercheurs=chercheurs.order_by('-date_modification'),
740 sites=sites.order_by('-date_maj'),
741 sites_auf=sites_auf
742 )
743
744 def url(self):
745 url = ''
746 if self.discipline:
747 url += '/discipline/%d' % self.discipline.id
748 if self.region:
749 url += '/region/%d' % self.region.id
750 url += '/recherche/'
751 if self.q:
752 url += '?' + urlencode({'q': self.q})
753 return url
754
755 class RessourceSearch(Search):
756 auteur = models.CharField(max_length=100, blank=True, verbose_name="auteur ou contributeur")
757 titre = models.CharField(max_length=100, blank=True)
758 sujet = models.CharField(max_length=100, blank=True)
759 publisher = models.CharField(max_length=100, blank=True, verbose_name="éditeur")
760
761 class Meta:
762 verbose_name = 'recherche de ressources'
763 verbose_name_plural = "recherches de ressources"
764
765 def run(self):
766 results = Record.objects
767 if self.q:
768 results = results.search(self.q)
769 if self.auteur:
770 results = results.add_to_query('@(creator,contributor) ' + self.auteur)
771 if self.titre:
772 results = results.add_to_query('@title ' + self.titre)
773 if self.sujet:
774 results = results.add_to_query('@subject ' + self.sujet)
775 if self.publisher:
776 results = results.add_to_query('@publisher ' + self.publisher)
777 if self.discipline:
778 results = results.filter_discipline(self.discipline)
779 if self.region:
780 results = results.filter_region(self.region)
781 if not self.q:
782 """Montrer les résultats les plus récents si on n'a pas fait
783 une recherche par mots-clés."""
784 results = results.order_by('-modified')
785 return results.all()
786
787 def url(self):
788 qs = self.query_string()
789 return reverse('ressources') + ('?' + qs if qs else '')
790
791 class ActualiteSearchBase(Search):
792 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
793 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
794
795 class Meta:
796 abstract = True
797
798 def run(self):
799 results = Actualite.objects
800 if self.q:
801 results = results.search(self.q)
802 if self.discipline:
803 results = results.filter_discipline(self.discipline)
804 if self.region:
805 results = results.filter_region(self.region)
806 if self.date_min:
807 results = results.filter_date(min=self.date_min)
808 if self.date_max:
809 results = results.filter_date(max=self.date_max)
810 return results.all()
811
812 class ActualiteSearch(ActualiteSearchBase):
813
814 class Meta:
815 verbose_name = "recherche d'actualités"
816 verbose_name_plural = "recherches d'actualités"
817
818 def run(self):
819 return super(ActualiteSearch, self).run().filter_type('actu')
820
821 def url(self):
822 qs = self.query_string()
823 return reverse('actualites') + ('?' + qs if qs else '')
824
825 class AppelSearch(ActualiteSearchBase):
826
827 class Meta:
828 verbose_name = "recherche d'appels d'offres"
829 verbose_name_plural = "recherches d'appels d'offres"
830
831 def run(self):
832 return super(AppelSearch, self).run().filter_type('appel')
833
834 def url(self):
835 qs = self.query_string()
836 return reverse('appels') + ('?' + qs if qs else '')
837
838 class EvenementSearch(Search):
839 titre = models.CharField(max_length=100, blank=True, verbose_name="Intitulé")
840 type = models.CharField(max_length=100, blank=True, choices=Evenement.TYPE_CHOICES)
841 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
842 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
843
844 class Meta:
845 verbose_name = "recherche d'évènements"
846 verbose_name_plural = "recherches d'évènements"
847
848 def run(self):
849 results = Evenement.objects
850 if self.q:
851 results = results.search(self.q)
852 if self.titre:
853 results = results.add_to_query('@titre ' + self.titre)
854 if self.discipline:
855 results = results.filter_discipline(self.discipline)
856 if self.region:
857 results = results.filter_region(self.region)
858 if self.type:
859 results = results.filter_type(self.type)
860 if self.date_min:
861 results = results.filter_debut(min=self.date_min)
862 if self.date_max:
863 results = results.filter_debut(max=self.date_max)
864 return results.all()
865
866 def url(self):
867 qs = self.query_string()
868 return reverse('agenda') + ('?' + qs if qs else '')
869