Supprimé des sources qui ne fonctionnent plus
[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 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
210 class 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
219 class 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
241 class 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
256 class 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
295 class 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
305 class 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
317 class 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
341 class 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
359 def 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
381 class 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
601 def 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()
607 pre_delete.connect(delete_vevent, sender=Evenement)
608
609
610 class 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
620 class 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
630 class 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
641 class RecordQuerySet(SEPQuerySet):
642
643 def filter_modified(self, min=None, max=None):
644 return self._filter_date('modified', min=min, max=max)
645
646
647 class 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
665 class 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 qs = RecordQuerySet(self.model)
671 qs = qs.filter(validated=True)
672 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
673 return qs.distinct()
674
675 def get_sphinx_query_set(self):
676 return RecordSphinxQuerySet(self.model)
677
678 def filter_modified(self, min=None, max=None):
679 return self.get_query_set().filter_modified(min=min, max=max)
680
681
682 class Record(models.Model):
683
684 #fonctionnement interne
685 id = models.AutoField(primary_key=True)
686 server = models.CharField(max_length=255, verbose_name=u'serveur')
687 last_update = models.CharField(max_length=255)
688 last_checksum = models.CharField(max_length=255)
689 validated = models.BooleanField(default=True, verbose_name=u'validé')
690
691 #OAI
692 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
693 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
694 description = models.TextField(null=True, blank=True)
695 modified = models.CharField(max_length=255, null=True, blank=True)
696 identifier = models.CharField(
697 max_length=255, null=True, blank=True, unique=True
698 )
699 uri = models.CharField(max_length=255, null=True, blank=True, unique=True)
700 source = models.TextField(null=True, blank=True)
701 contributor = models.TextField(null=True, blank=True)
702 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
703 publisher = models.TextField(null=True, blank=True)
704 type = models.TextField(null=True, blank=True)
705 format = models.TextField(null=True, blank=True)
706 language = models.TextField(null=True, blank=True)
707
708 listsets = models.ManyToManyField(ListSet, null=True, blank=True)
709
710 #SEP 2 (aucune données récoltées)
711 alt_title = models.TextField(null=True, blank=True)
712 abstract = models.TextField(null=True, blank=True)
713 creation = models.CharField(max_length=255, null=True, blank=True)
714 issued = models.CharField(max_length=255, null=True, blank=True)
715 isbn = models.TextField(null=True, blank=True)
716 orig_lang = models.TextField(null=True, blank=True)
717
718 categorie = models.ForeignKey(
719 RecordCategorie, blank=True, null=True, verbose_name='catégorie'
720 )
721
722 # Metadata AUF multivaluées
723 disciplines = models.ManyToManyField(Discipline, blank=True)
724 thematiques = models.ManyToManyField(
725 Thematique, blank=True, verbose_name='thématiques'
726 )
727 pays = models.ManyToManyField(Pays, blank=True)
728 regions = models.ManyToManyField(
729 Region, blank=True, verbose_name='régions'
730 )
731
732 # Managers
733 objects = RecordManager()
734 all_objects = models.Manager()
735
736 class Meta:
737 verbose_name = 'ressource'
738
739 def __unicode__(self):
740 return "[%s] %s" % (self.server, self.title)
741
742 def get_absolute_url(self):
743 return reverse('ressource', kwargs={'id': self.id})
744
745 def getServeurURL(self):
746 """Retourne l'URL du serveur de provenance"""
747 return RESOURCES[self.server]['url']
748
749 def est_complet(self):
750 """teste si le record à toutes les données obligatoires"""
751 return self.disciplines.count() > 0 and \
752 self.thematiques.count() > 0 and \
753 self.pays.count() > 0 and \
754 self.regions.count() > 0
755
756 def assigner_regions(self, regions):
757 self.regions.add(*regions)
758
759 def assigner_disciplines(self, disciplines):
760 self.disciplines.add(*disciplines)
761
762
763 class RecordEdit(Record):
764
765 class Meta:
766 proxy = True
767 verbose_name = 'ressource (édition)'
768 verbose_name_plural = 'ressources (édition)'
769
770
771 class Serveur(models.Model):
772 """Identification d'un serveur d'ou proviennent les références"""
773 nom = models.CharField(primary_key=True, max_length=255)
774
775 def __unicode__(self,):
776 return self.nom
777
778 def conf_2_db(self,):
779 for k in RESOURCES.keys():
780 s, created = Serveur.objects.get_or_create(nom=k)
781 s.nom = k
782 s.save()
783
784
785 class Profile(models.Model):
786 user = models.ForeignKey(User, unique=True)
787 serveurs = models.ManyToManyField(Serveur, null=True, blank=True)
788
789
790 class HarvestLog(models.Model):
791 context = models.CharField(max_length=255)
792 name = models.CharField(max_length=255)
793 date = models.DateTimeField(auto_now=True)
794 added = models.IntegerField(null=True, blank=True)
795 updated = models.IntegerField(null=True, blank=True)
796 processed = models.IntegerField(null=True, blank=True)
797 record = models.ForeignKey(Record, null=True, blank=True)
798
799 @staticmethod
800 def add(message):
801 logger = HarvestLog()
802 if 'record_id' in message:
803 message['record'] = Record.all_objects.get(id=message['record_id'])
804 del(message['record_id'])
805
806 for k, v in message.items():
807 setattr(logger, k, v)
808 logger.save()
809
810
811 # Pages statiques
812
813 class PageStatique(models.Model):
814 id = models.CharField(max_length=32, primary_key=True)
815 titre = models.CharField(max_length=100)
816 contenu = models.TextField()
817
818 class Meta:
819 verbose_name_plural = 'pages statiques'
820
821
822 # Recherches
823
824 class GlobalSearchResults(object):
825
826 def __init__(self, actualites=None, appels=None, evenements=None,
827 ressources=None, chercheurs=None, groupes=None,
828 sites=None, sites_auf=None):
829 self.actualites = actualites
830 self.appels = appels
831 self.evenements = evenements
832 self.ressources = ressources
833 self.chercheurs = chercheurs
834 self.groupes = groupes
835 self.sites = sites
836 self.sites_auf = sites_auf
837
838 def __nonzero__(self):
839 return bool(self.actualites or self.appels or self.evenements or
840 self.ressources or self.chercheurs or self.groupes or
841 self.sites or self.sites_auf)
842
843
844 class Search(models.Model):
845 user = models.ForeignKey(User, editable=False)
846 content_type = models.ForeignKey(ContentType, editable=False)
847 nom = models.CharField(max_length=100, verbose_name="nom de la recherche")
848 alerte_courriel = models.BooleanField(
849 verbose_name="Envoyer une alerte courriel"
850 )
851 derniere_alerte = models.DateField(
852 verbose_name="Date d'envoi de la dernière alerte courriel",
853 null=True, editable=False
854 )
855 q = models.CharField(
856 max_length=255, blank=True, verbose_name="dans tous les champs"
857 )
858 discipline = models.ForeignKey(Discipline, blank=True, null=True)
859 region = models.ForeignKey(
860 Region, blank=True, null=True, verbose_name='région',
861 help_text="La région est ici définie au sens, non strictement "
862 "géographique, du Bureau régional de l'AUF de référence."
863 )
864
865 def query_string(self):
866 params = dict()
867 for field in self._meta.fields:
868 if field.name in ['id', 'user', 'nom', 'search_ptr',
869 'content_type']:
870 continue
871 value = getattr(self, field.column)
872 if value:
873 if isinstance(value, datetime.date):
874 params[field.name] = value.strftime('%d/%m/%Y')
875 else:
876 params[field.name] = smart_str(value)
877 return urlencode(params)
878
879 class Meta:
880 verbose_name = 'recherche transversale'
881 verbose_name_plural = "recherches transversales"
882
883 def __unicode__(self):
884 return self.nom
885
886 def save(self):
887 if self.alerte_courriel:
888 try:
889 original_search = Search.objects.get(id=self.id)
890 if not original_search.alerte_courriel:
891 # On a nouvellement activé l'alerte courriel. Notons la
892 # date.
893 self.derniere_alerte = \
894 datetime.date.today() - datetime.timedelta(days=1)
895 except Search.DoesNotExist:
896 self.derniere_alerte = \
897 datetime.date.today() - datetime.timedelta(days=1)
898 if (not self.content_type_id):
899 self.content_type = ContentType.objects.get_for_model(
900 self.__class__
901 )
902 super(Search, self).save()
903
904 def as_leaf_class(self):
905 content_type = self.content_type
906 model = content_type.model_class()
907 if(model == Search):
908 return self
909 return model.objects.get(id=self.id)
910
911 def run(self, min_date=None, max_date=None):
912 from chercheurs.models import Chercheur, Groupe
913 from sitotheque.models import Site
914
915 actualites = Actualite.objects
916 evenements = Evenement.objects
917 ressources = Record.objects
918 chercheurs = Chercheur.objects
919 groupes = Groupe.objects
920 sites = Site.objects
921 if self.q:
922 actualites = actualites.search(self.q)
923 evenements = evenements.search(self.q)
924 ressources = ressources.search(self.q)
925 chercheurs = chercheurs.search(self.q)
926 groupes = groupes.search(self.q)
927 sites = sites.search(self.q)
928 if self.discipline:
929 actualites = actualites.filter_discipline(self.discipline)
930 evenements = evenements.filter_discipline(self.discipline)
931 ressources = ressources.filter_discipline(self.discipline)
932 chercheurs = chercheurs.filter_discipline(self.discipline)
933 sites = sites.filter_discipline(self.discipline)
934 if self.region:
935 actualites = actualites.filter_region(self.region)
936 evenements = evenements.filter_region(self.region)
937 ressources = ressources.filter_region(self.region)
938 chercheurs = chercheurs.filter_region(self.region)
939 sites = sites.filter_region(self.region)
940 if min_date:
941 actualites = actualites.filter_date(min=min_date)
942 evenements = evenements.filter_date_modification(min=min_date)
943 ressources = ressources.filter_modified(min=min_date)
944 chercheurs = chercheurs.filter_date_modification(min=min_date)
945 sites = sites.filter_date_maj(min=min_date)
946 if max_date:
947 actualites = actualites.filter_date(max=max_date)
948 evenements = evenements.filter_date_modification(max=max_date)
949 ressources = ressources.filter_modified(max=max_date)
950 chercheurs = chercheurs.filter_date_modification(max=max_date)
951 sites = sites.filter_date_maj(max=max_date)
952
953 try:
954 sites_auf = google_search(0, self.q)['results']
955 except:
956 sites_auf = []
957
958 return GlobalSearchResults(
959 actualites=actualites.order_by('-date').filter_type('actu'),
960 appels=actualites.order_by('-date').filter_type('appels'),
961 evenements=evenements.order_by('-debut'),
962 ressources=ressources.order_by('-modified'),
963 chercheurs=chercheurs.order_by('-date_modification'),
964 groupes=groupes.order_by('nom'),
965 sites=sites.order_by('-date_maj'),
966 sites_auf=sites_auf
967 )
968
969 def url(self):
970
971 if self.content_type.model != 'search':
972 obj = self.content_type.get_object_for_this_type(pk=self.pk)
973 return obj.url()
974
975 url = ''
976 if self.discipline:
977 url += '/discipline/%d' % self.discipline.id
978 if self.region:
979 url += '/region/%d' % self.region.id
980 url += '/recherche/'
981 if self.q:
982 url += '?' + urlencode({'q': smart_str(self.q)})
983 return url
984
985 def rss_url(self):
986 return None
987
988 def send_email_alert(self):
989 """Envoie une alerte courriel correspondant à cette recherche"""
990 yesterday = datetime.date.today() - datetime.timedelta(days=1)
991 if self.derniere_alerte is not None:
992 results = self.as_leaf_class().run(
993 min_date=self.derniere_alerte, max_date=yesterday
994 )
995 if results:
996 subject = 'Savoirs en partage - ' + self.nom
997 from_email = CONTACT_EMAIL
998 to_email = self.user.email
999 text_content = u'Voici les derniers résultats ' \
1000 u'correspondant à votre recherche sauvegardée.\n\n'
1001 text_content += self.as_leaf_class() \
1002 .get_email_alert_content(results)
1003 text_content += u'''
1004
1005 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
1006 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (
1007 SITE_ROOT_URL, reverse('recherches')
1008 )
1009 html_content = \
1010 '<div style="font-family: Arial, sans-serif">\n' + \
1011 markdown(smart_str(text_content)) + '</div>\n'
1012 msg = EmailMultiAlternatives(
1013 subject, text_content, from_email, [to_email]
1014 )
1015 msg.attach_alternative(html_content, "text/html")
1016 msg.send()
1017 self.derniere_alerte = yesterday
1018 self.save()
1019
1020 def get_email_alert_content(self, results):
1021 content = ''
1022 if results.chercheurs:
1023 content += u'\n### Nouveaux chercheurs\n\n'
1024 for chercheur in results.chercheurs:
1025 content += u'- [%s %s](%s%s) \n' % (
1026 chercheur.nom.upper(), chercheur.prenom, SITE_ROOT_URL,
1027 chercheur.get_absolute_url()
1028 )
1029 content += u' %s\n\n' % chercheur.etablissement_display
1030 if results.ressources:
1031 content += u'\n### Nouvelles ressources\n\n'
1032 for ressource in results.ressources:
1033 content += u'- [%s](%s%s)\n\n' % (
1034 ressource.title, SITE_ROOT_URL,
1035 ressource.get_absolute_url()
1036 )
1037 if ressource.description:
1038 content += '\n'
1039 content += ''.join(
1040 ' %s\n' % line
1041 for line in textwrap.wrap(ressource.description)
1042 )
1043 content += '\n'
1044
1045 if results.actualites:
1046 content += u'\n### Nouvelles actualités\n\n'
1047 for actualite in results.actualites:
1048 content += u'- [%s](%s%s)\n\n' % (
1049 actualite.titre, SITE_ROOT_URL,
1050 actualite.get_absolute_url()
1051 )
1052 if actualite.texte:
1053 content += '\n'
1054 content += ''.join(
1055 ' %s\n' % line
1056 for line in textwrap.wrap(actualite.texte)
1057 )
1058 content += '\n'
1059 if results.appels:
1060 content += u"\n### Nouveaux appels d'offres\n\n"
1061 for appel in results.appels:
1062 content += u'- [%s](%s%s)\n\n' % (appel.titre,
1063 SITE_ROOT_URL,
1064 appel.get_absolute_url())
1065 if appel.texte:
1066 content += '\n'
1067 content += ''.join(
1068 ' %s\n' % line
1069 for line in textwrap.wrap(appel.texte)
1070 )
1071 content += '\n'
1072 if results.evenements:
1073 content += u"\n### Nouveaux évènements\n\n"
1074 for evenement in results.evenements:
1075 content += u'- [%s](%s%s) \n' % (
1076 evenement.titre, SITE_ROOT_URL,
1077 evenement.get_absolute_url()
1078 )
1079 content += u' où ? : %s \n' % evenement.lieu
1080 content += evenement.debut.strftime(
1081 ' quand ? : %d/%m/%Y %H:%M \n'
1082 )
1083 content += u' durée ? : %s\n\n' % \
1084 evenement.duration_display()
1085 content += u' quoi ? : '
1086 content += '\n '.join(
1087 textwrap.wrap(evenement.description)
1088 )
1089 content += '\n\n'
1090 if results.sites:
1091 content += u"\n### Nouveaux sites\n\n"
1092 for site in results.sites:
1093 content += u'- [%s](%s%s)\n\n' % (site.titre,
1094 SITE_ROOT_URL,
1095 site.get_absolute_url())
1096 if site.description:
1097 content += '\n'
1098 content += ''.join(
1099 ' %s\n' % line
1100 for line in textwrap.wrap(site.description)
1101 )
1102 content += '\n'
1103 return content
1104
1105
1106 class RessourceSearch(Search):
1107 auteur = models.CharField(
1108 max_length=100, blank=True, verbose_name="auteur ou contributeur"
1109 )
1110 titre = models.CharField(max_length=100, blank=True)
1111 sujet = models.CharField(max_length=100, blank=True)
1112 publisher = models.CharField(
1113 max_length=100, blank=True, verbose_name="éditeur"
1114 )
1115 categorie = models.ForeignKey(
1116 RecordCategorie, blank=True, null=True, verbose_name='catégorie'
1117 )
1118
1119 class Meta:
1120 verbose_name = 'recherche de ressources'
1121 verbose_name_plural = "recherches de ressources"
1122
1123 def run(self, min_date=None, max_date=None):
1124 results = Record.objects
1125 if self.q:
1126 results = results.search(self.q)
1127 if self.auteur:
1128 results = results.add_to_query(
1129 '@(creator,contributor) ' + self.auteur
1130 )
1131 if self.titre:
1132 results = results.add_to_query('@title ' + self.titre)
1133 if self.sujet:
1134 results = results.add_to_query('@subject ' + self.sujet)
1135 if self.publisher:
1136 results = results.add_to_query('@publisher ' + self.publisher)
1137 if self.categorie:
1138 results = results.add_to_query('@categorie %s' % self.categorie.id)
1139 if self.discipline:
1140 results = results.filter_discipline(self.discipline)
1141 if self.region:
1142 results = results.filter_region(self.region)
1143 if min_date:
1144 results = results.filter_modified(min=min_date)
1145 if max_date:
1146 results = results.filter_modified(max=max_date)
1147 if not self.q:
1148 """Montrer les résultats les plus récents si on n'a pas fait
1149 une recherche par mots-clés."""
1150 results = results.order_by('-modified')
1151 return results.all()
1152
1153 def url(self):
1154 qs = self.query_string()
1155 return reverse('ressources') + ('?' + qs if qs else '')
1156
1157 def rss_url(self):
1158 qs = self.query_string()
1159 return reverse('rss_ressources') + ('?' + qs if qs else '')
1160
1161 def get_email_alert_content(self, results):
1162 content = ''
1163 for ressource in results:
1164 content += u'- [%s](%s%s)\n\n' % (ressource.title,
1165 SITE_ROOT_URL,
1166 ressource.get_absolute_url())
1167 if ressource.description:
1168 content += '\n'
1169 content += ''.join(
1170 ' %s\n' % line
1171 for line in textwrap.wrap(ressource.description)
1172 )
1173 content += '\n'
1174 return content
1175
1176
1177 class ActualiteSearchBase(Search):
1178 date_min = models.DateField(
1179 blank=True, null=True, verbose_name="depuis le"
1180 )
1181 date_max = models.DateField(
1182 blank=True, null=True, verbose_name="jusqu'au"
1183 )
1184
1185 class Meta:
1186 abstract = True
1187
1188 def run(self, min_date=None, max_date=None):
1189 results = Actualite.objects
1190 if self.q:
1191 results = results.search(self.q)
1192 if self.discipline:
1193 results = results.filter_discipline(self.discipline)
1194 if self.region:
1195 results = results.filter_region(self.region)
1196 if self.date_min:
1197 results = results.filter_date(min=self.date_min)
1198 if self.date_max:
1199 results = results.filter_date(max=self.date_max)
1200 if min_date:
1201 results = results.filter_date(min=min_date)
1202 if max_date:
1203 results = results.filter_date(max=max_date)
1204 return results.all()
1205
1206 def get_email_alert_content(self, results):
1207 content = ''
1208 for actualite in results:
1209 content += u'- [%s](%s%s)\n\n' % (actualite.titre,
1210 SITE_ROOT_URL,
1211 actualite.get_absolute_url())
1212 if actualite.texte:
1213 content += '\n'
1214 content += ''.join(
1215 ' %s\n' % line
1216 for line in textwrap.wrap(actualite.texte)
1217 )
1218 content += '\n'
1219 return content
1220
1221
1222 class ActualiteSearch(ActualiteSearchBase):
1223
1224 class Meta:
1225 verbose_name = "recherche d'actualités"
1226 verbose_name_plural = "recherches d'actualités"
1227
1228 def run(self, min_date=None, max_date=None):
1229 return super(ActualiteSearch, self) \
1230 .run(min_date=min_date, max_date=max_date) \
1231 .filter_type('actu')
1232
1233 def url(self):
1234 qs = self.query_string()
1235 return reverse('actualites') + ('?' + qs if qs else '')
1236
1237 def rss_url(self):
1238 qs = self.query_string()
1239 return reverse('rss_actualites') + ('?' + qs if qs else '')
1240
1241
1242 class AppelSearch(ActualiteSearchBase):
1243
1244 class Meta:
1245 verbose_name = "recherche d'appels d'offres"
1246 verbose_name_plural = "recherches d'appels d'offres"
1247
1248 def run(self, min_date=None, max_date=None):
1249 return super(AppelSearch, self) \
1250 .run(min_date=min_date, max_date=max_date) \
1251 .filter_type('appels')
1252
1253 def url(self):
1254 qs = self.query_string()
1255 return reverse('appels') + ('?' + qs if qs else '')
1256
1257 def rss_url(self):
1258 qs = self.query_string()
1259 return reverse('rss_appels') + ('?' + qs if qs else '')
1260
1261
1262 class EvenementSearch(Search):
1263 titre = models.CharField(
1264 max_length=100, blank=True, verbose_name="Intitulé"
1265 )
1266 type = models.CharField(
1267 max_length=100, blank=True, choices=Evenement.TYPE_CHOICES
1268 )
1269 date_min = models.DateField(
1270 blank=True, null=True, verbose_name="depuis le"
1271 )
1272 date_max = models.DateField(
1273 blank=True, null=True, verbose_name="jusqu'au"
1274 )
1275
1276 class Meta:
1277 verbose_name = "recherche d'évènements"
1278 verbose_name_plural = "recherches d'évènements"
1279
1280 def run(self, min_date=None, max_date=None):
1281 results = Evenement.objects
1282 if self.q:
1283 results = results.search(self.q)
1284 if self.titre:
1285 results = results.add_to_query('@titre ' + self.titre)
1286 if self.discipline:
1287 results = results.filter_discipline(self.discipline)
1288 if self.region:
1289 results = results.filter_region(self.region)
1290 if self.type:
1291 results = results.filter_type(self.type)
1292 if self.date_min:
1293 results = results.filter_debut(min=self.date_min)
1294 if self.date_max:
1295 results = results.filter_debut(max=self.date_max)
1296 if min_date:
1297 results = results.filter_date_modification(min=min_date)
1298 if max_date:
1299 results = results.filter_date_modification(max=max_date)
1300 return results.all()
1301
1302 def url(self):
1303 qs = self.query_string()
1304 return reverse('agenda') + ('?' + qs if qs else '')
1305
1306 def rss_url(self):
1307 qs = self.query_string()
1308 return reverse('rss_agenda') + ('?' + qs if qs else '')
1309
1310 def get_email_alert_content(self, results):
1311 content = ''
1312 for evenement in results:
1313 content += u'- [%s](%s%s) \n' % (evenement.titre,
1314 SITE_ROOT_URL,
1315 evenement.get_absolute_url())
1316 content += u' où ? : %s \n' % evenement.lieu
1317 content += evenement.debut.strftime(
1318 ' quand ? : %d/%m/%Y %H:%M \n'
1319 )
1320 content += u' durée ? : %s\n\n' % evenement.duration_display()
1321 content += u' quoi ? : '
1322 content += '\n '.join(
1323 textwrap.wrap(evenement.description)
1324 )
1325 content += '\n\n'
1326 return content