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