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