Optimisation de la requête des ressources valides
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
... / ...
CommitLineData
1# -*- encoding: utf-8 -*-
2
3import caldav
4import datetime
5import feedparser
6import os
7import pytz
8import random
9import textwrap
10import uuid
11import vobject
12from pytz.tzinfo import AmbiguousTimeError, NonExistentTimeError
13from urllib import urlencode
14
15from backend_config import RESOURCES
16from babel.dates import get_timezone_name
17from caldav.lib import error
18from django.contrib.auth.models import User
19from django.contrib.contenttypes.models import ContentType
20from django.core.mail import EmailMultiAlternatives
21from django.core.urlresolvers import reverse
22from django.db import models
23from django.db.models import Q
24from django.db.models.signals import pre_delete
25from django.utils.encoding import smart_unicode, smart_str
26from djangosphinx.models import SphinxQuerySet, SearchError
27from markdown2 import markdown
28
29from auf.django.references.models import Region, Pays, Thematique
30from settings import CALENDRIER_URL, SITE_ROOT_URL, CONTACT_EMAIL
31from lib.calendrier import combine
32from lib.recherche import google_search
33
34
35# Fonctionnalités communes à tous les query sets
36
37class RandomQuerySetMixin(object):
38 """Mixin pour les modèles.
39
40 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
41 méthode pour récupérer des objets au hasard.
42 """
43
44 def random(self, n=1):
45 """Récupère aléatoirement un nombre donné d'objets."""
46 count = self.count()
47 positions = random.sample(xrange(count), min(n, count))
48 return [self[p] for p in positions]
49
50
51class SEPQuerySet(models.query.QuerySet, RandomQuerySetMixin):
52
53 def _filter_date(self, field, min=None, max=None):
54 """Limite les résultats à ceux dont le champ ``field`` tombe entre
55 les dates ``min`` et ``max``."""
56 qs = self
57 if min:
58 qs = qs.filter(**{field + '__gte': min})
59 if max:
60 qs = qs.filter(**{
61 field + '__lt': max + datetime.timedelta(days=1)
62 })
63 return qs
64
65
66class SEPSphinxQuerySet(SphinxQuerySet, RandomQuerySetMixin):
67 """Fonctionnalités communes aux query sets de Sphinx."""
68
69 def __init__(self, model=None, index=None, weights=None):
70 SphinxQuerySet.__init__(self, model=model, index=index,
71 mode='SPH_MATCH_EXTENDED2',
72 rankmode='SPH_RANK_PROXIMITY_BM25',
73 weights=weights)
74
75 def add_to_query(self, query):
76 """Ajoute une partie à la requête texte."""
77
78 # Assurons-nous qu'il y a un nombre pair de guillemets
79 if query.count('"') % 2 != 0:
80 # Sinon, on enlève le dernier (faut choisir...)
81 i = query.rindex('"')
82 query = query[:i] + query[i + 1:]
83
84 new_query = smart_unicode(self._query) + ' ' + query \
85 if self._query else query
86 return self.query(new_query)
87
88 def search(self, text):
89 """Recherche ``text`` dans tous les champs."""
90 return self.add_to_query('@* ' + text)
91
92 def filter_discipline(self, discipline):
93 """Par défaut, le filtre par discipline cherche le nom de la
94 discipline dans tous les champs."""
95 return self.search('"%s"' % discipline.nom)
96
97 def filter_region(self, region):
98 """Par défaut, le filtre par région cherche le nom de la région dans
99 tous les champs."""
100 return self.search('"%s"' % region.nom)
101
102 def _filter_date(self, field, min=None, max=None):
103 """Limite les résultats à ceux dont le champ ``field`` tombe entre
104 les dates ``min`` et ``max``."""
105 qs = self
106 if min:
107 qs = qs.filter(**{field + '__gte': min.toordinal() + 365})
108 if max:
109 qs = qs.filter(**{field + '__lte': max.toordinal() + 365})
110 return qs
111
112 def _get_sphinx_results(self):
113 try:
114 return SphinxQuerySet._get_sphinx_results(self)
115 except SearchError:
116 # Essayons d'enlever les caractères qui peuvent poser problème.
117 for c in '|!@()~/<=^$':
118 self._query = self._query.replace(c, ' ')
119 try:
120 return SphinxQuerySet._get_sphinx_results(self)
121 except SearchError:
122 # Ça ne marche toujours pas. Enlevons les guillemets et les
123 # tirets.
124 for c in '"-':
125 self._query = self._query.replace(c, ' ')
126 return SphinxQuerySet._get_sphinx_results(self)
127
128
129class SEPManager(models.Manager):
130 """Lorsque les méthodes ``search``, ``filter_region`` et
131 ``filter_discipline`` sont appelées sur ce manager, le query set
132 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
133
134 def query(self, query):
135 return self.get_sphinx_query_set().query(query)
136
137 def add_to_query(self, query):
138 return self.get_sphinx_query_set().add_to_query(query)
139
140 def search(self, text):
141 return self.get_sphinx_query_set().search(text)
142
143 def filter_region(self, region):
144 return self.get_sphinx_query_set().filter_region(region)
145
146 def filter_discipline(self, discipline):
147 return self.get_sphinx_query_set().filter_discipline(discipline)
148
149
150# Disciplines
151
152class Discipline(models.Model):
153 id = models.IntegerField(primary_key=True, db_column='id_discipline')
154 nom = models.CharField(max_length=765, db_column='nom_discipline')
155
156 def __unicode__(self):
157 return self.nom
158
159 class Meta:
160 db_table = u'discipline'
161 ordering = ["nom"]
162
163
164# Actualités
165
166class SourceActualite(models.Model):
167 TYPE_CHOICES = (
168 ('actu', 'Actualités'),
169 ('appels', "Appels d'offres"),
170 )
171
172 nom = models.CharField(max_length=255)
173 url = models.CharField(max_length=255, verbose_name='URL', blank=True)
174 type = models.CharField(
175 max_length=10, default='actu', choices=TYPE_CHOICES
176 )
177 region = models.ForeignKey(Region, null=True, blank=True, verbose_name='région')
178
179 class Meta:
180 verbose_name = u'fil RSS syndiqué'
181 verbose_name_plural = u'fils RSS syndiqués'
182
183 def __unicode__(self,):
184 return u"%s (%s)" % (self.nom, self.get_type_display())
185
186 def update(self):
187 """Mise à jour du fil RSS."""
188 if not self.url:
189 return
190 feed = feedparser.parse(self.url)
191 for entry in feed.entries:
192 if not all(
193 x in entry for x in ('link', 'published_parsed', 'title')
194 ):
195 continue
196 if Actualite.all_objects.filter(url=entry.link).count() == 0:
197 ts = entry.get('published_parsed')
198 date = datetime.date(ts.tm_year, ts.tm_mon, ts.tm_mday) \
199 if ts else datetime.date.today()
200 texte = entry.summary_detail.value \
201 if 'summary_detail' in entry else ''
202 actualite = self.actualites.create(
203 titre=entry.title, texte=texte, url=entry.link,
204 date=date
205 )
206 if self.region:
207 actualite.regions.add(self.region)
208
209
210class ActualiteQuerySet(SEPQuerySet):
211
212 def filter_date(self, min=None, max=None):
213 return self._filter_date('date', min=min, max=max)
214
215 def filter_type(self, type):
216 return self.filter(source__type=type)
217
218
219class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
220 TYPE_CODES = {'actu': 1, 'appels': 2}
221
222 def __init__(self, model=None):
223 SEPSphinxQuerySet.__init__(
224 self, model=model, index='savoirsenpartage_actualites',
225 weights=dict(titre=3)
226 )
227
228 def filter_date(self, min=None, max=None):
229 return self._filter_date('date', min=min, max=max)
230
231 def filter_type(self, type):
232 return self.filter(type=self.TYPE_CODES[type])
233
234 def filter_region(self, region):
235 return self.filter(region_ids=region.id)
236
237 def filter_discipline(self, discipline):
238 return self.filter(discipline_ids=discipline.id)
239
240
241class ActualiteManager(SEPManager):
242
243 def get_query_set(self):
244 return ActualiteQuerySet(self.model).filter(visible=True)
245
246 def get_sphinx_query_set(self):
247 return ActualiteSphinxQuerySet(self.model).order_by('-date')
248
249 def filter_date(self, min=None, max=None):
250 return self.get_query_set().filter_date(min=min, max=max)
251
252 def filter_type(self, type):
253 return self.get_query_set().filter_type(type)
254
255
256class Actualite(models.Model):
257 id = models.AutoField(primary_key=True, db_column='id_actualite')
258 titre = models.CharField(max_length=765, db_column='titre_actualite')
259 texte = models.TextField(db_column='texte_actualite')
260 url = models.CharField(max_length=765, db_column='url_actualite')
261 date = models.DateField(db_column='date_actualite')
262 visible = models.BooleanField(db_column='visible_actualite', default=False)
263 ancienid = models.IntegerField(
264 db_column='ancienId_actualite', blank=True, null=True
265 )
266 source = models.ForeignKey(SourceActualite, related_name='actualites')
267 disciplines = models.ManyToManyField(
268 Discipline, blank=True, related_name="actualites"
269 )
270 regions = models.ManyToManyField(
271 Region, blank=True, related_name="actualites",
272 verbose_name='régions'
273 )
274
275 objects = ActualiteManager()
276 all_objects = models.Manager()
277
278 class Meta:
279 db_table = u'actualite'
280 ordering = ["-date"]
281
282 def __unicode__(self):
283 return "%s" % (self.titre)
284
285 def get_absolute_url(self):
286 return reverse('actualite', kwargs={'id': self.id})
287
288 def assigner_disciplines(self, disciplines):
289 self.disciplines.add(*disciplines)
290
291 def assigner_regions(self, regions):
292 self.regions.add(*regions)
293
294
295class ActualiteVoir(Actualite):
296
297 class Meta:
298 proxy = True
299 verbose_name = 'actualité (visualisation)'
300 verbose_name_plural = 'actualités (visualisation)'
301
302
303# Agenda
304
305class EvenementQuerySet(SEPQuerySet):
306
307 def filter_type(self, type):
308 return self.filter(type=type)
309
310 def filter_debut(self, min=None, max=None):
311 return self._filter_date('debut', min=min, max=max)
312
313 def filter_date_modification(self, min=None, max=None):
314 return self._filter_date('date_modification', min=min, max=max)
315
316
317class EvenementSphinxQuerySet(SEPSphinxQuerySet):
318
319 def __init__(self, model=None):
320 SEPSphinxQuerySet.__init__(
321 self, model=model, index='savoirsenpartage_evenements',
322 weights=dict(titre=3)
323 )
324
325 def filter_type(self, type):
326 return self.add_to_query('@type "%s"' % type)
327
328 def filter_debut(self, min=None, max=None):
329 return self._filter_date('debut', min=min, max=max)
330
331 def filter_date_modification(self, min=None, max=None):
332 return self._filter_date('date_modification', min=min, max=max)
333
334 def filter_region(self, region):
335 return self.add_to_query('@regions "%s"' % region.nom)
336
337 def filter_discipline(self, discipline):
338 return self.add_to_query('@disciplines "%s"' % discipline.nom)
339
340
341class EvenementManager(SEPManager):
342
343 def get_query_set(self):
344 return EvenementQuerySet(self.model).filter(approuve=True)
345
346 def get_sphinx_query_set(self):
347 return EvenementSphinxQuerySet(self.model).order_by('-debut')
348
349 def filter_type(self, type):
350 return self.get_query_set().filter_type(type)
351
352 def filter_debut(self, min=None, max=None):
353 return self.get_query_set().filter_debut(min=min, max=max)
354
355 def filter_date_modification(self, min=None, max=None):
356 return self.get_query_set().filter_date_modification(min=min, max=max)
357
358
359def build_time_zone_choices(pays=None):
360 timezones = pytz.country_timezones[pays] if pays else pytz.common_timezones
361 result = []
362 now = datetime.datetime.now()
363 for tzname in timezones:
364 tz = pytz.timezone(tzname)
365 fr_name = get_timezone_name(tz, locale='fr_FR')
366 try:
367 offset = tz.utcoffset(now)
368 except (AmbiguousTimeError, NonExistentTimeError):
369 # oups. On est en train de changer d'heure. Ça devrait être fini
370 # demain
371 offset = tz.utcoffset(now + datetime.timedelta(days=1))
372 seconds = offset.seconds + offset.days * 86400
373 (hours, minutes) = divmod(seconds // 60, 60)
374 offset_str = 'UTC%+d:%d' % (hours, minutes) \
375 if minutes else 'UTC%+d' % hours
376 result.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
377 result.sort()
378 return [(x[1], x[2]) for x in result]
379
380
381class Evenement(models.Model):
382 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
383 (u'Conférence', u'Conférence'),
384 (u'Appel à contribution', u'Appel à contribution'),
385 (u'Journée d\'étude', u'Journée d\'étude'),
386 (u'Autre', u'Autre'))
387 TIME_ZONE_CHOICES = build_time_zone_choices()
388
389 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
390 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
391 titre = models.CharField(max_length=255)
392 discipline = models.ForeignKey(
393 'Discipline', related_name="discipline",
394 blank=True, null=True
395 )
396 discipline_secondaire = models.ForeignKey(
397 'Discipline', related_name="discipline_secondaire",
398 verbose_name=u"discipline secondaire", blank=True, null=True
399 )
400 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
401 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
402 adresse = models.TextField()
403 ville = models.CharField(max_length=100)
404 pays = models.ForeignKey(
405 Pays, null=True, related_name='evenements', to_field='code'
406 )
407 debut = models.DateTimeField(default=datetime.datetime.now)
408 fin = models.DateTimeField(default=datetime.datetime.now)
409 fuseau = models.CharField(
410 max_length=100, choices=TIME_ZONE_CHOICES,
411 verbose_name='fuseau horaire'
412 )
413 description = models.TextField()
414 contact = models.TextField(null=True) # champ obsolète
415 prenom = models.CharField('prénom', max_length=100)
416 nom = models.CharField(max_length=100)
417 courriel = models.EmailField()
418 url = models.CharField(max_length=255, blank=True, null=True)
419 piece_jointe = models.FileField(
420 upload_to='agenda/pj', blank=True, verbose_name='pièce jointe'
421 )
422 regions = models.ManyToManyField(
423 Region, blank=True, related_name="evenements",
424 verbose_name='régions additionnelles',
425 help_text="On considère d'emblée que l'évènement se déroule dans la "
426 "région dans laquelle se trouve le pays indiqué plus haut. Il est "
427 "possible de désigner ici des régions additionnelles."
428 )
429 date_modification = models.DateTimeField(
430 editable=False, auto_now=True, null=True
431 )
432
433 objects = EvenementManager()
434 all_objects = models.Manager()
435
436 class Meta:
437 ordering = ['-debut']
438 verbose_name = u'évènement'
439 verbose_name_plural = u'évènements'
440
441 def __unicode__(self):
442 return "[%s] %s" % (self.uid, self.titre)
443
444 def get_absolute_url(self):
445 return reverse('evenement', kwargs={'id': self.id})
446
447 def duration_display(self):
448 delta = self.fin - self.debut
449 minutes, seconds = divmod(delta.seconds, 60)
450 hours, minutes = divmod(minutes, 60)
451 days = delta.days
452 parts = []
453 if days == 1:
454 parts.append('1 jour')
455 elif days > 1:
456 parts.append('%d jours' % days)
457 if hours == 1:
458 parts.append('1 heure')
459 elif hours > 1:
460 parts.append('%d heures' % hours)
461 if minutes == 1:
462 parts.append('1 minute')
463 elif minutes > 1:
464 parts.append('%d minutes' % minutes)
465 return ' '.join(parts)
466
467 def piece_jointe_display(self):
468 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
469
470 def courriel_display(self):
471 return self.courriel.replace(u'@', u' (à) ')
472
473 @property
474 def lieu(self):
475 bits = []
476 if self.adresse:
477 bits.append(self.adresse)
478 if self.ville:
479 bits.append(self.ville)
480 if self.pays:
481 bits.append(self.pays.nom)
482 return ', '.join(bits)
483
484 def clean(self):
485 from django.core.exceptions import ValidationError
486 if self.debut > self.fin:
487 raise ValidationError(
488 'La date de fin ne doit pas être antérieure à la date de début'
489 )
490
491 def save(self, *args, **kwargs):
492 """
493 Sauvegarde l'objet dans django et le synchronise avec caldav s'il a
494 été approuvé.
495 """
496 self.contact = '' # Vider ce champ obsolète à la première occasion...
497 self.clean()
498 super(Evenement, self).save(*args, **kwargs)
499 self.update_vevent()
500
501 # methodes de commnunications avec CALDAV
502 def as_ical(self,):
503 """Retourne l'evenement django sous forme d'objet icalendar"""
504 cal = vobject.iCalendar()
505 cal.add('vevent')
506
507 # fournit son propre uid
508 if self.uid in [None, ""]:
509 self.uid = str(uuid.uuid1())
510
511 cal.vevent.add('uid').value = self.uid
512
513 cal.vevent.add('summary').value = self.titre
514
515 if self.mots_cles is None:
516 kw = []
517 else:
518 kw = self.mots_cles.split(",")
519
520 try:
521 kw.append(self.discipline.nom)
522 kw.append(self.discipline_secondaire.nom)
523 kw.append(self.type)
524 except:
525 pass
526
527 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
528 for k in kw:
529 cal.vevent.add('x-auf-keywords').value = k
530
531 description = self.description
532 if len(kw) > 0:
533 if len(self.description) > 0:
534 description += "\n"
535 description += u"Mots-clés: " + ", ".join(kw)
536
537 cal.vevent.add('dtstart').value = \
538 combine(self.debut, pytz.timezone(self.fuseau))
539 cal.vevent.add('dtend').value = \
540 combine(self.fin, pytz.timezone(self.fuseau))
541 cal.vevent.add('created').value = \
542 combine(datetime.datetime.now(), "UTC")
543 cal.vevent.add('dtstamp').value = \
544 combine(datetime.datetime.now(), "UTC")
545 if len(description) > 0:
546 cal.vevent.add('description').value = description
547 if len(self.contact) > 0:
548 cal.vevent.add('contact').value = self.contact
549 if len(self.url) > 0:
550 cal.vevent.add('url').value = self.url
551 cal.vevent.add('location').value = ', '.join(
552 x for x in [self.adresse, self.ville, self.pays.nom] if x
553 )
554 if self.piece_jointe:
555 url = self.piece_jointe.url
556 if not url.startswith('http://'):
557 url = SITE_ROOT_URL + url
558 cal.vevent.add('attach').value = url
559 return cal
560
561 def update_vevent(self,):
562 """Essaie de créer l'évènement sur le serveur ical.
563 En cas de succès, l'évènement local devient donc inactif et approuvé"""
564 try:
565 if self.approuve:
566 event = self.as_ical()
567 client = caldav.DAVClient(CALENDRIER_URL)
568 cal = caldav.Calendar(client, url=CALENDRIER_URL)
569 e = caldav.Event(
570 client, parent=cal, data=event.serialize(), id=self.uid
571 )
572 e.save()
573 except:
574 self.approuve = False
575
576 def delete_vevent(self,):
577 """Supprime l'evenement sur le serveur caldav"""
578 try:
579 if self.approuve:
580 client = caldav.DAVClient(CALENDRIER_URL)
581 cal = caldav.Calendar(client, url=CALENDRIER_URL)
582 e = cal.event(self.uid)
583 e.delete()
584 except error.NotFoundError:
585 pass
586
587 def assigner_regions(self, regions):
588 self.regions.add(*regions)
589
590 def assigner_disciplines(self, disciplines):
591 if len(disciplines) == 1:
592 if self.discipline:
593 self.discipline_secondaire = disciplines[0]
594 else:
595 self.discipline = disciplines[0]
596 elif len(disciplines) >= 2:
597 self.discipline = disciplines[0]
598 self.discipline_secondaire = disciplines[1]
599
600
601def delete_vevent(sender, instance, *args, **kwargs):
602 # Surcharge du comportement de suppression
603 # La méthode de connexion par signals est préférable à surcharger la
604 # méthode delete() car dans le cas de la suppression par lots, cell-ci
605 # n'est pas invoquée
606 instance.delete_vevent()
607pre_delete.connect(delete_vevent, sender=Evenement)
608
609
610class EvenementVoir(Evenement):
611
612 class Meta:
613 proxy = True
614 verbose_name = 'événement (visualisation)'
615 verbose_name_plural = 'événement (visualisation)'
616
617
618# Ressources
619
620class ListSet(models.Model):
621 spec = models.CharField(primary_key=True, max_length=255)
622 name = models.CharField(max_length=255)
623 server = models.CharField(max_length=255)
624 validated = models.BooleanField(default=True)
625
626 def __unicode__(self,):
627 return self.name
628
629
630class RecordCategorie(models.Model):
631 nom = models.CharField(max_length=255)
632
633 class Meta:
634 verbose_name = 'catégorie ressource'
635 verbose_name_plural = 'catégories ressources'
636
637 def __unicode__(self):
638 return self.nom
639
640
641class RecordQuerySet(SEPQuerySet):
642
643 def filter_modified(self, min=None, max=None):
644 return self._filter_date('modified', min=min, max=max)
645
646
647class RecordSphinxQuerySet(SEPSphinxQuerySet):
648
649 def __init__(self, model=None):
650 SEPSphinxQuerySet.__init__(
651 self, model=model, index='savoirsenpartage_ressources',
652 weights=dict(title=3)
653 )
654
655 def filter_modified(self, min=None, max=None):
656 return self._filter_date('modified', min=min, max=max)
657
658 def filter_region(self, region):
659 return self.filter(region_ids=region.id)
660
661 def filter_discipline(self, discipline):
662 return self.filter(discipline_ids=discipline.id)
663
664
665class RecordManager(SEPManager):
666
667 def get_query_set(self):
668 """Ne garder que les ressources validées et qui sont soit dans aucun
669 listset ou au moins dans un listset validé."""
670 ids_valides = ListSet.objects.filter(validated=True) \
671 .values('record__id')
672 ids_invalides = ListSet.objects.filter(validated=False) \
673 .values('record__id')
674 qs = RecordQuerySet(self.model)
675 qs = qs.filter(validated=True)
676 qs = qs.filter(Q(id__in=ids_valides) | ~Q(id__in=ids_invalides))
677 return qs
678
679 def get_sphinx_query_set(self):
680 return RecordSphinxQuerySet(self.model)
681
682 def filter_modified(self, min=None, max=None):
683 return self.get_query_set().filter_modified(min=min, max=max)
684
685
686class Record(models.Model):
687
688 #fonctionnement interne
689 id = models.AutoField(primary_key=True)
690 server = models.CharField(max_length=255, verbose_name=u'serveur')
691 last_update = models.CharField(max_length=255)
692 last_checksum = models.CharField(max_length=255)
693 validated = models.BooleanField(default=True, verbose_name=u'validé')
694
695 #OAI
696 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
697 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
698 description = models.TextField(null=True, blank=True)
699 modified = models.CharField(max_length=255, null=True, blank=True)
700 identifier = models.CharField(
701 max_length=255, null=True, blank=True, unique=True
702 )
703 uri = models.CharField(max_length=255, null=True, blank=True, unique=True)
704 source = models.TextField(null=True, blank=True)
705 contributor = models.TextField(null=True, blank=True)
706 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
707 publisher = models.TextField(null=True, blank=True)
708 type = models.TextField(null=True, blank=True)
709 format = models.TextField(null=True, blank=True)
710 language = models.TextField(null=True, blank=True)
711
712 listsets = models.ManyToManyField(ListSet, null=True, blank=True)
713
714 #SEP 2 (aucune données récoltées)
715 alt_title = models.TextField(null=True, blank=True)
716 abstract = models.TextField(null=True, blank=True)
717 creation = models.CharField(max_length=255, null=True, blank=True)
718 issued = models.CharField(max_length=255, null=True, blank=True)
719 isbn = models.TextField(null=True, blank=True)
720 orig_lang = models.TextField(null=True, blank=True)
721
722 categorie = models.ForeignKey(
723 RecordCategorie, blank=True, null=True, verbose_name='catégorie'
724 )
725
726 # Metadata AUF multivaluées
727 disciplines = models.ManyToManyField(Discipline, blank=True)
728 thematiques = models.ManyToManyField(
729 Thematique, blank=True, verbose_name='thématiques'
730 )
731 pays = models.ManyToManyField(Pays, blank=True)
732 regions = models.ManyToManyField(
733 Region, blank=True, verbose_name='régions'
734 )
735
736 # Managers
737 objects = RecordManager()
738 all_objects = models.Manager()
739
740 class Meta:
741 verbose_name = 'ressource'
742
743 def __unicode__(self):
744 return "[%s] %s" % (self.server, self.title)
745
746 def get_absolute_url(self):
747 return reverse('ressource', kwargs={'id': self.id})
748
749 def getServeurURL(self):
750 """Retourne l'URL du serveur de provenance"""
751 return RESOURCES[self.server]['url'] \
752 if self.server in RESOURCES else ''
753
754 def est_complet(self):
755 """teste si le record à toutes les données obligatoires"""
756 return self.disciplines.count() > 0 and \
757 self.thematiques.count() > 0 and \
758 self.pays.count() > 0 and \
759 self.regions.count() > 0
760
761 def assigner_regions(self, regions):
762 self.regions.add(*regions)
763
764 def assigner_disciplines(self, disciplines):
765 self.disciplines.add(*disciplines)
766
767
768class RecordEdit(Record):
769
770 class Meta:
771 proxy = True
772 verbose_name = 'ressource (édition)'
773 verbose_name_plural = 'ressources (édition)'
774
775
776class Serveur(models.Model):
777 """Identification d'un serveur d'ou proviennent les références"""
778 nom = models.CharField(primary_key=True, max_length=255)
779
780 def __unicode__(self,):
781 return self.nom
782
783 def conf_2_db(self,):
784 for k in RESOURCES.keys():
785 s, created = Serveur.objects.get_or_create(nom=k)
786 s.nom = k
787 s.save()
788
789
790class Profile(models.Model):
791 user = models.ForeignKey(User, unique=True)
792 serveurs = models.ManyToManyField(Serveur, null=True, blank=True)
793
794
795class HarvestLog(models.Model):
796 context = models.CharField(max_length=255)
797 name = models.CharField(max_length=255)
798 date = models.DateTimeField(auto_now=True)
799 added = models.IntegerField(null=True, blank=True)
800 updated = models.IntegerField(null=True, blank=True)
801 processed = models.IntegerField(null=True, blank=True)
802 record = models.ForeignKey(Record, null=True, blank=True)
803
804 @staticmethod
805 def add(message):
806 logger = HarvestLog()
807 if 'record_id' in message:
808 message['record'] = Record.all_objects.get(id=message['record_id'])
809 del(message['record_id'])
810
811 for k, v in message.items():
812 setattr(logger, k, v)
813 logger.save()
814
815
816# Pages statiques
817
818class PageStatique(models.Model):
819 id = models.CharField(max_length=32, primary_key=True)
820 titre = models.CharField(max_length=100)
821 contenu = models.TextField()
822
823 class Meta:
824 verbose_name_plural = 'pages statiques'
825
826
827# Recherches
828
829class GlobalSearchResults(object):
830
831 def __init__(self, actualites=None, appels=None, evenements=None,
832 ressources=None, chercheurs=None, groupes=None,
833 sites=None, sites_auf=None):
834 self.actualites = actualites
835 self.appels = appels
836 self.evenements = evenements
837 self.ressources = ressources
838 self.chercheurs = chercheurs
839 self.groupes = groupes
840 self.sites = sites
841 self.sites_auf = sites_auf
842
843 def __nonzero__(self):
844 return bool(self.actualites or self.appels or self.evenements or
845 self.ressources or self.chercheurs or self.groupes or
846 self.sites or self.sites_auf)
847
848
849class Search(models.Model):
850 user = models.ForeignKey(User, editable=False)
851 content_type = models.ForeignKey(ContentType, editable=False)
852 nom = models.CharField(max_length=100, verbose_name="nom de la recherche")
853 alerte_courriel = models.BooleanField(
854 verbose_name="Envoyer une alerte courriel"
855 )
856 derniere_alerte = models.DateField(
857 verbose_name="Date d'envoi de la dernière alerte courriel",
858 null=True, editable=False
859 )
860 q = models.CharField(
861 max_length=255, blank=True, verbose_name="dans tous les champs"
862 )
863 discipline = models.ForeignKey(Discipline, blank=True, null=True)
864 region = models.ForeignKey(
865 Region, blank=True, null=True, verbose_name='région',
866 help_text="La région est ici définie au sens, non strictement "
867 "géographique, du Bureau régional de l'AUF de référence."
868 )
869
870 def query_string(self):
871 params = dict()
872 for field in self._meta.fields:
873 if field.name in ['id', 'user', 'nom', 'search_ptr',
874 'content_type']:
875 continue
876 value = getattr(self, field.column)
877 if value:
878 if isinstance(value, datetime.date):
879 params[field.name] = value.strftime('%d/%m/%Y')
880 else:
881 params[field.name] = smart_str(value)
882 return urlencode(params)
883
884 class Meta:
885 verbose_name = 'recherche transversale'
886 verbose_name_plural = "recherches transversales"
887
888 def __unicode__(self):
889 return self.nom
890
891 def save(self):
892 if self.alerte_courriel:
893 try:
894 original_search = Search.objects.get(id=self.id)
895 if not original_search.alerte_courriel:
896 # On a nouvellement activé l'alerte courriel. Notons la
897 # date.
898 self.derniere_alerte = \
899 datetime.date.today() - datetime.timedelta(days=1)
900 except Search.DoesNotExist:
901 self.derniere_alerte = \
902 datetime.date.today() - datetime.timedelta(days=1)
903 if (not self.content_type_id):
904 self.content_type = ContentType.objects.get_for_model(
905 self.__class__
906 )
907 super(Search, self).save()
908
909 def as_leaf_class(self):
910 content_type = self.content_type
911 model = content_type.model_class()
912 if(model == Search):
913 return self
914 return model.objects.get(id=self.id)
915
916 def run(self, min_date=None, max_date=None):
917 from chercheurs.models import Chercheur, Groupe
918 from sitotheque.models import Site
919
920 actualites = Actualite.objects
921 evenements = Evenement.objects
922 ressources = Record.objects
923 chercheurs = Chercheur.objects
924 groupes = Groupe.objects
925 sites = Site.objects
926 if self.q:
927 actualites = actualites.search(self.q)
928 evenements = evenements.search(self.q)
929 ressources = ressources.search(self.q)
930 chercheurs = chercheurs.search(self.q)
931 groupes = groupes.search(self.q)
932 sites = sites.search(self.q)
933 if self.discipline:
934 actualites = actualites.filter_discipline(self.discipline)
935 evenements = evenements.filter_discipline(self.discipline)
936 ressources = ressources.filter_discipline(self.discipline)
937 chercheurs = chercheurs.filter_discipline(self.discipline)
938 sites = sites.filter_discipline(self.discipline)
939 if self.region:
940 actualites = actualites.filter_region(self.region)
941 evenements = evenements.filter_region(self.region)
942 ressources = ressources.filter_region(self.region)
943 chercheurs = chercheurs.filter_region(self.region)
944 sites = sites.filter_region(self.region)
945 if min_date:
946 actualites = actualites.filter_date(min=min_date)
947 evenements = evenements.filter_date_modification(min=min_date)
948 ressources = ressources.filter_modified(min=min_date)
949 chercheurs = chercheurs.filter_date_modification(min=min_date)
950 sites = sites.filter_date_maj(min=min_date)
951 if max_date:
952 actualites = actualites.filter_date(max=max_date)
953 evenements = evenements.filter_date_modification(max=max_date)
954 ressources = ressources.filter_modified(max=max_date)
955 chercheurs = chercheurs.filter_date_modification(max=max_date)
956 sites = sites.filter_date_maj(max=max_date)
957
958 try:
959 sites_auf = google_search(0, self.q)['results']
960 except:
961 sites_auf = []
962
963 return GlobalSearchResults(
964 actualites=actualites.order_by('-date').filter_type('actu'),
965 appels=actualites.order_by('-date').filter_type('appels'),
966 evenements=evenements.order_by('-debut'),
967 ressources=ressources.order_by('-modified'),
968 chercheurs=chercheurs.order_by('-date_modification'),
969 groupes=groupes.order_by('nom'),
970 sites=sites.order_by('-date_maj'),
971 sites_auf=sites_auf
972 )
973
974 def url(self):
975
976 if self.content_type.model != 'search':
977 obj = self.content_type.get_object_for_this_type(pk=self.pk)
978 return obj.url()
979
980 url = ''
981 if self.discipline:
982 url += '/discipline/%d' % self.discipline.id
983 if self.region:
984 url += '/region/%d' % self.region.id
985 url += '/recherche/'
986 if self.q:
987 url += '?' + urlencode({'q': smart_str(self.q)})
988 return url
989
990 def rss_url(self):
991 return None
992
993 def send_email_alert(self):
994 """Envoie une alerte courriel correspondant à cette recherche"""
995 yesterday = datetime.date.today() - datetime.timedelta(days=1)
996 if self.derniere_alerte is not None:
997 results = self.as_leaf_class().run(
998 min_date=self.derniere_alerte, max_date=yesterday
999 )
1000 if results:
1001 subject = 'Savoirs en partage - ' + self.nom
1002 from_email = CONTACT_EMAIL
1003 to_email = self.user.email
1004 text_content = u'Voici les derniers résultats ' \
1005 u'correspondant à votre recherche sauvegardée.\n\n'
1006 text_content += self.as_leaf_class() \
1007 .get_email_alert_content(results)
1008 text_content += u'''
1009
1010Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
1011rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (
1012 SITE_ROOT_URL, reverse('recherches')
1013 )
1014 html_content = \
1015 '<div style="font-family: Arial, sans-serif">\n' + \
1016 markdown(smart_str(text_content)) + '</div>\n'
1017 msg = EmailMultiAlternatives(
1018 subject, text_content, from_email, [to_email]
1019 )
1020 msg.attach_alternative(html_content, "text/html")
1021 msg.send()
1022 self.derniere_alerte = yesterday
1023 self.save()
1024
1025 def get_email_alert_content(self, results):
1026 content = ''
1027 if results.chercheurs:
1028 content += u'\n### Nouveaux chercheurs\n\n'
1029 for chercheur in results.chercheurs:
1030 content += u'- [%s %s](%s%s) \n' % (
1031 chercheur.nom.upper(), chercheur.prenom, SITE_ROOT_URL,
1032 chercheur.get_absolute_url()
1033 )
1034 content += u' %s\n\n' % chercheur.etablissement_display
1035 if results.ressources:
1036 content += u'\n### Nouvelles ressources\n\n'
1037 for ressource in results.ressources:
1038 content += u'- [%s](%s%s)\n\n' % (
1039 ressource.title, SITE_ROOT_URL,
1040 ressource.get_absolute_url()
1041 )
1042 if ressource.description:
1043 content += '\n'
1044 content += ''.join(
1045 ' %s\n' % line
1046 for line in textwrap.wrap(ressource.description)
1047 )
1048 content += '\n'
1049
1050 if results.actualites:
1051 content += u'\n### Nouvelles actualités\n\n'
1052 for actualite in results.actualites:
1053 content += u'- [%s](%s%s)\n\n' % (
1054 actualite.titre, SITE_ROOT_URL,
1055 actualite.get_absolute_url()
1056 )
1057 if actualite.texte:
1058 content += '\n'
1059 content += ''.join(
1060 ' %s\n' % line
1061 for line in textwrap.wrap(actualite.texte)
1062 )
1063 content += '\n'
1064 if results.appels:
1065 content += u"\n### Nouveaux appels d'offres\n\n"
1066 for appel in results.appels:
1067 content += u'- [%s](%s%s)\n\n' % (appel.titre,
1068 SITE_ROOT_URL,
1069 appel.get_absolute_url())
1070 if appel.texte:
1071 content += '\n'
1072 content += ''.join(
1073 ' %s\n' % line
1074 for line in textwrap.wrap(appel.texte)
1075 )
1076 content += '\n'
1077 if results.evenements:
1078 content += u"\n### Nouveaux évènements\n\n"
1079 for evenement in results.evenements:
1080 content += u'- [%s](%s%s) \n' % (
1081 evenement.titre, SITE_ROOT_URL,
1082 evenement.get_absolute_url()
1083 )
1084 content += u' où ? : %s \n' % evenement.lieu
1085 content += evenement.debut.strftime(
1086 ' quand ? : %d/%m/%Y %H:%M \n'
1087 )
1088 content += u' durée ? : %s\n\n' % \
1089 evenement.duration_display()
1090 content += u' quoi ? : '
1091 content += '\n '.join(
1092 textwrap.wrap(evenement.description)
1093 )
1094 content += '\n\n'
1095 if results.sites:
1096 content += u"\n### Nouveaux sites\n\n"
1097 for site in results.sites:
1098 content += u'- [%s](%s%s)\n\n' % (site.titre,
1099 SITE_ROOT_URL,
1100 site.get_absolute_url())
1101 if site.description:
1102 content += '\n'
1103 content += ''.join(
1104 ' %s\n' % line
1105 for line in textwrap.wrap(site.description)
1106 )
1107 content += '\n'
1108 return content
1109
1110
1111class RessourceSearch(Search):
1112 auteur = models.CharField(
1113 max_length=100, blank=True, verbose_name="auteur ou contributeur"
1114 )
1115 titre = models.CharField(max_length=100, blank=True)
1116 sujet = models.CharField(max_length=100, blank=True)
1117 publisher = models.CharField(
1118 max_length=100, blank=True, verbose_name="éditeur"
1119 )
1120 categorie = models.ForeignKey(
1121 RecordCategorie, blank=True, null=True, verbose_name='catégorie'
1122 )
1123
1124 class Meta:
1125 verbose_name = 'recherche de ressources'
1126 verbose_name_plural = "recherches de ressources"
1127
1128 def run(self, min_date=None, max_date=None):
1129 results = Record.objects
1130 if self.q:
1131 results = results.search(self.q)
1132 if self.auteur:
1133 results = results.add_to_query(
1134 '@(creator,contributor) ' + self.auteur
1135 )
1136 if self.titre:
1137 results = results.add_to_query('@title ' + self.titre)
1138 if self.sujet:
1139 results = results.add_to_query('@subject ' + self.sujet)
1140 if self.publisher:
1141 results = results.add_to_query('@publisher ' + self.publisher)
1142 if self.categorie:
1143 results = results.add_to_query('@categorie %s' % self.categorie.id)
1144 if self.discipline:
1145 results = results.filter_discipline(self.discipline)
1146 if self.region:
1147 results = results.filter_region(self.region)
1148 if min_date:
1149 results = results.filter_modified(min=min_date)
1150 if max_date:
1151 results = results.filter_modified(max=max_date)
1152 if not self.q:
1153 """Montrer les résultats les plus récents si on n'a pas fait
1154 une recherche par mots-clés."""
1155 results = results.order_by('-modified')
1156 return results.all()
1157
1158 def url(self):
1159 qs = self.query_string()
1160 return reverse('ressources') + ('?' + qs if qs else '')
1161
1162 def rss_url(self):
1163 qs = self.query_string()
1164 return reverse('rss_ressources') + ('?' + qs if qs else '')
1165
1166 def get_email_alert_content(self, results):
1167 content = ''
1168 for ressource in results:
1169 content += u'- [%s](%s%s)\n\n' % (ressource.title,
1170 SITE_ROOT_URL,
1171 ressource.get_absolute_url())
1172 if ressource.description:
1173 content += '\n'
1174 content += ''.join(
1175 ' %s\n' % line
1176 for line in textwrap.wrap(ressource.description)
1177 )
1178 content += '\n'
1179 return content
1180
1181
1182class ActualiteSearchBase(Search):
1183 date_min = models.DateField(
1184 blank=True, null=True, verbose_name="depuis le"
1185 )
1186 date_max = models.DateField(
1187 blank=True, null=True, verbose_name="jusqu'au"
1188 )
1189
1190 class Meta:
1191 abstract = True
1192
1193 def run(self, min_date=None, max_date=None):
1194 results = Actualite.objects
1195 if self.q:
1196 results = results.search(self.q)
1197 if self.discipline:
1198 results = results.filter_discipline(self.discipline)
1199 if self.region:
1200 results = results.filter_region(self.region)
1201 if self.date_min:
1202 results = results.filter_date(min=self.date_min)
1203 if self.date_max:
1204 results = results.filter_date(max=self.date_max)
1205 if min_date:
1206 results = results.filter_date(min=min_date)
1207 if max_date:
1208 results = results.filter_date(max=max_date)
1209 return results.all()
1210
1211 def get_email_alert_content(self, results):
1212 content = ''
1213 for actualite in results:
1214 content += u'- [%s](%s%s)\n\n' % (actualite.titre,
1215 SITE_ROOT_URL,
1216 actualite.get_absolute_url())
1217 if actualite.texte:
1218 content += '\n'
1219 content += ''.join(
1220 ' %s\n' % line
1221 for line in textwrap.wrap(actualite.texte)
1222 )
1223 content += '\n'
1224 return content
1225
1226
1227class ActualiteSearch(ActualiteSearchBase):
1228
1229 class Meta:
1230 verbose_name = "recherche d'actualités"
1231 verbose_name_plural = "recherches d'actualités"
1232
1233 def run(self, min_date=None, max_date=None):
1234 return super(ActualiteSearch, self) \
1235 .run(min_date=min_date, max_date=max_date) \
1236 .filter_type('actu')
1237
1238 def url(self):
1239 qs = self.query_string()
1240 return reverse('actualites') + ('?' + qs if qs else '')
1241
1242 def rss_url(self):
1243 qs = self.query_string()
1244 return reverse('rss_actualites') + ('?' + qs if qs else '')
1245
1246
1247class AppelSearch(ActualiteSearchBase):
1248
1249 class Meta:
1250 verbose_name = "recherche d'appels d'offres"
1251 verbose_name_plural = "recherches d'appels d'offres"
1252
1253 def run(self, min_date=None, max_date=None):
1254 return super(AppelSearch, self) \
1255 .run(min_date=min_date, max_date=max_date) \
1256 .filter_type('appels')
1257
1258 def url(self):
1259 qs = self.query_string()
1260 return reverse('appels') + ('?' + qs if qs else '')
1261
1262 def rss_url(self):
1263 qs = self.query_string()
1264 return reverse('rss_appels') + ('?' + qs if qs else '')
1265
1266
1267class EvenementSearch(Search):
1268 titre = models.CharField(
1269 max_length=100, blank=True, verbose_name="Intitulé"
1270 )
1271 type = models.CharField(
1272 max_length=100, blank=True, choices=Evenement.TYPE_CHOICES
1273 )
1274 date_min = models.DateField(
1275 blank=True, null=True, verbose_name="depuis le"
1276 )
1277 date_max = models.DateField(
1278 blank=True, null=True, verbose_name="jusqu'au"
1279 )
1280
1281 class Meta:
1282 verbose_name = "recherche d'évènements"
1283 verbose_name_plural = "recherches d'évènements"
1284
1285 def run(self, min_date=None, max_date=None):
1286 results = Evenement.objects
1287 if self.q:
1288 results = results.search(self.q)
1289 if self.titre:
1290 results = results.add_to_query('@titre ' + self.titre)
1291 if self.discipline:
1292 results = results.filter_discipline(self.discipline)
1293 if self.region:
1294 results = results.filter_region(self.region)
1295 if self.type:
1296 results = results.filter_type(self.type)
1297 if self.date_min:
1298 results = results.filter_debut(min=self.date_min)
1299 if self.date_max:
1300 results = results.filter_debut(max=self.date_max)
1301 if min_date:
1302 results = results.filter_date_modification(min=min_date)
1303 if max_date:
1304 results = results.filter_date_modification(max=max_date)
1305 return results.all()
1306
1307 def url(self):
1308 qs = self.query_string()
1309 return reverse('agenda') + ('?' + qs if qs else '')
1310
1311 def rss_url(self):
1312 qs = self.query_string()
1313 return reverse('rss_agenda') + ('?' + qs if qs else '')
1314
1315 def get_email_alert_content(self, results):
1316 content = ''
1317 for evenement in results:
1318 content += u'- [%s](%s%s) \n' % (evenement.titre,
1319 SITE_ROOT_URL,
1320 evenement.get_absolute_url())
1321 content += u' où ? : %s \n' % evenement.lieu
1322 content += evenement.debut.strftime(
1323 ' quand ? : %d/%m/%Y %H:%M \n'
1324 )
1325 content += u' durée ? : %s\n\n' % evenement.duration_display()
1326 content += u' quoi ? : '
1327 content += '\n '.join(
1328 textwrap.wrap(evenement.description)
1329 )
1330 content += '\n\n'
1331 return content