7e66dac8e7490447c1d8453b574dee49548c8eb0
[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 # Agenda
253
254 class EvenementQuerySet(SEPQuerySet):
255
256 def filter_type(self, type):
257 return self.filter(type=type)
258
259 def filter_debut(self, min=None, max=None):
260 return self._filter_date('debut', min=min, max=max)
261
262 def filter_date_modification(self, min=None, max=None):
263 return self._filter_date('date_modification', min=min, max=max)
264
265 class EvenementSphinxQuerySet(SEPSphinxQuerySet):
266
267 def __init__(self, model=None):
268 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_evenements',
269 weights=dict(titre=3))
270
271 def filter_type(self, type):
272 return self.add_to_query('@type "%s"' % type)
273
274 def filter_debut(self, min=None, max=None):
275 return self._filter_date('debut', min=min, max=max)
276
277 def filter_date_modification(self, min=None, max=None):
278 return self._filter_date('date_modification', min=min, max=max)
279
280 class EvenementManager(SEPManager):
281
282 def get_query_set(self):
283 return EvenementQuerySet(self.model).filter(approuve=True)
284
285 def get_sphinx_query_set(self):
286 return EvenementSphinxQuerySet(self.model).order_by('-debut')
287
288 def filter_type(self, type):
289 return self.get_query_set().filter_type(type)
290
291 def filter_debut(self, min=None, max=None):
292 return self.get_query_set().filter_debut(min=min, max=max)
293
294 def filter_date_modification(self, min=None, max=None):
295 return self.get_query_set().filter_date_modification(min=min, max=max)
296
297 def build_time_zone_choices(pays=None):
298 timezones = pytz.country_timezones[pays] if pays else pytz.common_timezones
299 result = []
300 now = datetime.datetime.now()
301 for tzname in timezones:
302 tz = pytz.timezone(tzname)
303 fr_name = get_timezone_name(tz, locale='fr_FR')
304 try:
305 offset = tz.utcoffset(now)
306 except (AmbiguousTimeError, NonExistentTimeError):
307 # oups. On est en train de changer d'heure. Ça devrait être fini
308 # demain
309 offset = tz.utcoffset(now + datetime.timedelta(days=1))
310 seconds = offset.seconds + offset.days * 86400
311 (hours, minutes) = divmod(seconds // 60, 60)
312 offset_str = 'UTC%+d:%d' % (hours, minutes) if minutes else 'UTC%+d' % hours
313 result.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
314 result.sort()
315 return [(x[1], x[2]) for x in result]
316
317 class Evenement(models.Model):
318 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
319 (u'Conférence', u'Conférence'),
320 (u'Appel à contribution', u'Appel à contribution'),
321 (u'Journée d\'étude', u'Journée d\'étude'),
322 (u'Autre', u'Autre'))
323 TIME_ZONE_CHOICES = build_time_zone_choices()
324
325 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
326 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
327 titre = models.CharField(max_length=255)
328 discipline = models.ForeignKey('Discipline', related_name = "discipline",
329 blank = True, null = True)
330 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
331 verbose_name=u"discipline secondaire",
332 blank=True, null=True)
333 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
334 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
335 adresse = models.TextField()
336 ville = models.CharField(max_length=100)
337 pays = models.ForeignKey(Pays, null=True, related_name='evenements')
338 debut = models.DateTimeField(default=datetime.datetime.now)
339 fin = models.DateTimeField(default=datetime.datetime.now)
340 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
341 description = models.TextField()
342 contact = models.TextField(null=True) # champ obsolète
343 prenom = models.CharField('prénom', max_length=100)
344 nom = models.CharField(max_length=100)
345 courriel = models.EmailField()
346 url = models.CharField(max_length=255, blank=True, null=True)
347 piece_jointe = models.FileField(upload_to='agenda/pj', blank=True, verbose_name='pièce jointe')
348 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions additionnelles',
349 help_text="On considère d'emblée que l'évènement se déroule dans la région "
350 "dans laquelle se trouve le pays indiqué plus haut. Il est possible "
351 "de désigner ici des régions additionnelles.")
352 date_modification = models.DateTimeField(editable=False, auto_now=True, null=True)
353
354 objects = EvenementManager()
355 all_objects = models.Manager()
356
357 class Meta:
358 ordering = ['-debut']
359 verbose_name = u'évènement'
360 verbose_name_plural = u'évènements'
361
362 def __unicode__(self):
363 return "[%s] %s" % (self.uid, self.titre)
364
365 def get_absolute_url(self):
366 return reverse('evenement', kwargs={'id': self.id})
367
368 def duration_display(self):
369 delta = self.fin - self.debut
370 minutes, seconds = divmod(delta.seconds, 60)
371 hours, minutes = divmod(minutes, 60)
372 days = delta.days
373 parts = []
374 if days == 1:
375 parts.append('1 jour')
376 elif days > 1:
377 parts.append('%d jours' % days)
378 if hours == 1:
379 parts.append('1 heure')
380 elif hours > 1:
381 parts.append('%d heures' % hours)
382 if minutes == 1:
383 parts.append('1 minute')
384 elif minutes > 1:
385 parts.append('%d minutes' % minutes)
386 return ' '.join(parts)
387
388 def piece_jointe_display(self):
389 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
390
391 def courriel_display(self):
392 return self.courriel.replace(u'@', u' (à) ')
393
394 @property
395 def lieu(self):
396 bits = []
397 if self.adresse:
398 bits.append(self.adresse)
399 if self.ville:
400 bits.append(self.ville)
401 if self.pays:
402 bits.append(self.pays.nom)
403 return ', '.join(bits)
404
405 def clean(self):
406 from django.core.exceptions import ValidationError
407 if self.debut > self.fin:
408 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
409
410 def save(self, *args, **kwargs):
411 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
412 approuvé"""
413 self.contact = '' # Vider ce champ obsolète à la première occasion...
414 self.clean()
415 super(Evenement, self).save(*args, **kwargs)
416 self.update_vevent()
417
418 # methodes de commnunications avec CALDAV
419 def as_ical(self,):
420 """Retourne l'evenement django sous forme d'objet icalendar"""
421 cal = vobject.iCalendar()
422 cal.add('vevent')
423
424 # fournit son propre uid
425 if self.uid in [None, ""]:
426 self.uid = str(uuid.uuid1())
427
428 cal.vevent.add('uid').value = self.uid
429
430 cal.vevent.add('summary').value = self.titre
431
432 if self.mots_cles is None:
433 kw = []
434 else:
435 kw = self.mots_cles.split(",")
436
437 try:
438 kw.append(self.discipline.nom)
439 kw.append(self.discipline_secondaire.nom)
440 kw.append(self.type)
441 except: pass
442
443 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
444 for k in kw:
445 cal.vevent.add('x-auf-keywords').value = k
446
447 description = self.description
448 if len(kw) > 0:
449 if len(self.description) > 0:
450 description += "\n"
451 description += u"Mots-clés: " + ", ".join(kw)
452
453 cal.vevent.add('dtstart').value = combine(self.debut, pytz.timezone(self.fuseau))
454 cal.vevent.add('dtend').value = combine(self.fin, pytz.timezone(self.fuseau))
455 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
456 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
457 if len(description) > 0:
458 cal.vevent.add('description').value = description
459 if len(self.contact) > 0:
460 cal.vevent.add('contact').value = self.contact
461 if len(self.url) > 0:
462 cal.vevent.add('url').value = self.url
463 cal.vevent.add('location').value = ', '.join([x for x in [self.adresse, self.ville, self.pays.nom] if x])
464 if self.piece_jointe:
465 url = self.piece_jointe.url
466 if not url.startswith('http://'):
467 url = SITE_ROOT_URL + url
468 cal.vevent.add('attach').value = url
469 return cal
470
471 def update_vevent(self,):
472 """Essaie de créer l'évènement sur le serveur ical.
473 En cas de succès, l'évènement local devient donc inactif et approuvé"""
474 try:
475 if self.approuve:
476 event = self.as_ical()
477 client = caldav.DAVClient(CALENDRIER_URL)
478 cal = caldav.Calendar(client, url = CALENDRIER_URL)
479 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
480 e.save()
481 except:
482 self.approuve = False
483
484 def delete_vevent(self,):
485 """Supprime l'evenement sur le serveur caldav"""
486 try:
487 if self.approuve:
488 event = self.as_ical()
489 client = caldav.DAVClient(CALENDRIER_URL)
490 cal = caldav.Calendar(client, url = CALENDRIER_URL)
491 e = cal.event(self.uid)
492 e.delete()
493 except error.NotFoundError:
494 pass
495
496 def assigner_regions(self, regions):
497 self.regions.add(*regions)
498
499 def assigner_disciplines(self, disciplines):
500 if len(disciplines) == 1:
501 if self.discipline:
502 self.discipline_secondaire = disciplines[0]
503 else:
504 self.discipline = disciplines[0]
505 elif len(disciplines) >= 2:
506 self.discipline = disciplines[0]
507 self.discipline_secondaire = disciplines[1]
508
509 def delete_vevent(sender, instance, *args, **kwargs):
510 # Surcharge du comportement de suppression
511 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
512 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
513 instance.delete_vevent()
514 pre_delete.connect(delete_vevent, sender=Evenement)
515
516 # Ressources
517
518 class ListSet(models.Model):
519 spec = models.CharField(primary_key = True, max_length = 255)
520 name = models.CharField(max_length = 255)
521 server = models.CharField(max_length = 255)
522 validated = models.BooleanField(default = True)
523
524 def __unicode__(self,):
525 return self.name
526
527 class RecordCategorie(models.Model):
528 nom = models.CharField(max_length=255)
529
530 class Meta:
531 verbose_name = 'catégorie ressource'
532 verbose_name_plural = 'catégories ressources'
533
534 def __unicode__(self):
535 return self.nom
536
537
538 class RecordQuerySet(SEPQuerySet):
539
540 def filter_modified(self, min=None, max=None):
541 return self._filter_date('modified', min=min, max=max)
542
543 class RecordSphinxQuerySet(SEPSphinxQuerySet):
544
545 def __init__(self, model=None):
546 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_ressources',
547 weights=dict(title=3))
548
549 def filter_modified(self, min=None, max=None):
550 return self._filter_date('modified', min=min, max=max)
551
552 class RecordManager(SEPManager):
553
554 def get_query_set(self):
555 """Ne garder que les ressources validées et qui sont soit dans aucun
556 listset ou au moins dans un listset validé."""
557 qs = RecordQuerySet(self.model)
558 qs = qs.filter(validated=True)
559 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
560 return qs.distinct()
561
562 def get_sphinx_query_set(self):
563 return RecordSphinxQuerySet(self.model)
564
565 def filter_modified(self, min=None, max=None):
566 return self.get_query_set().filter_modified(min=min, max=max)
567
568 class Record(models.Model):
569
570 #fonctionnement interne
571 id = models.AutoField(primary_key = True)
572 server = models.CharField(max_length = 255, verbose_name=u'serveur')
573 last_update = models.CharField(max_length = 255)
574 last_checksum = models.CharField(max_length = 255)
575 validated = models.BooleanField(default=True, verbose_name=u'validé')
576
577 #OAI
578 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
579 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
580 description = models.TextField(null=True, blank=True)
581 modified = models.CharField(max_length=255, null=True, blank=True)
582 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
583 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
584 source = models.TextField(null = True, blank = True)
585 contributor = models.TextField(null = True, blank = True)
586 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
587 publisher = models.TextField(null = True, blank = True)
588 type = models.TextField(null = True, blank = True)
589 format = models.TextField(null = True, blank = True)
590 language = models.TextField(null = True, blank = True)
591
592 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
593
594 #SEP 2 (aucune données récoltées)
595 alt_title = models.TextField(null = True, blank = True)
596 abstract = models.TextField(null = True, blank = True)
597 creation = models.CharField(max_length = 255, null = True, blank = True)
598 issued = models.CharField(max_length = 255, null = True, blank = True)
599 isbn = models.TextField(null = True, blank = True)
600 orig_lang = models.TextField(null = True, blank = True)
601
602 categorie = models.ForeignKey(RecordCategorie, blank=True, null=True, verbose_name='catégorie')
603
604 # Metadata AUF multivaluées
605 disciplines = models.ManyToManyField(Discipline, blank=True)
606 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
607 pays = models.ManyToManyField(Pays, blank=True)
608 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
609
610 # Managers
611 objects = RecordManager()
612 all_objects = models.Manager()
613
614 class Meta:
615 verbose_name = 'ressource'
616
617 def __unicode__(self):
618 return "[%s] %s" % (self.server, self.title)
619
620 def get_absolute_url(self):
621 return reverse('ressource', kwargs={'id': self.id})
622
623 def getServeurURL(self):
624 """Retourne l'URL du serveur de provenance"""
625 return RESOURCES[self.server]['url']
626
627 def est_complet(self):
628 """teste si le record à toutes les données obligatoires"""
629 return self.disciplines.count() > 0 and \
630 self.thematiques.count() > 0 and \
631 self.pays.count() > 0 and \
632 self.regions.count() > 0
633
634 def assigner_regions(self, regions):
635 self.regions.add(*regions)
636
637 def assigner_disciplines(self, disciplines):
638 self.disciplines.add(*disciplines)
639
640 class Serveur(models.Model):
641 """Identification d'un serveur d'ou proviennent les références"""
642 nom = models.CharField(primary_key = True, max_length = 255)
643
644 def __unicode__(self,):
645 return self.nom
646
647 def conf_2_db(self,):
648 for k in RESOURCES.keys():
649 s, created = Serveur.objects.get_or_create(nom=k)
650 s.nom = k
651 s.save()
652
653 class Profile(models.Model):
654 user = models.ForeignKey(User, unique=True)
655 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
656
657 class HarvestLog(models.Model):
658 context = models.CharField(max_length = 255)
659 name = models.CharField(max_length = 255)
660 date = models.DateTimeField(auto_now = True)
661 added = models.IntegerField(null = True, blank = True)
662 updated = models.IntegerField(null = True, blank = True)
663 processed = models.IntegerField(null = True, blank = True)
664 record = models.ForeignKey(Record, null = True, blank = True)
665
666 @staticmethod
667 def add(message):
668 logger = HarvestLog()
669 if message.has_key('record_id'):
670 message['record'] = Record.all_objects.get(id=message['record_id'])
671 del(message['record_id'])
672
673 for k,v in message.items():
674 setattr(logger, k, v)
675 logger.save()
676
677 # Pages statiques
678
679 class PageStatique(models.Model):
680 id = models.CharField(max_length=32, primary_key=True)
681 titre = models.CharField(max_length=100)
682 contenu = models.TextField()
683
684 class Meta:
685 verbose_name_plural = 'pages statiques'
686
687 # Recherches
688
689 class GlobalSearchResults(object):
690
691 def __init__(self, actualites=None, appels=None, evenements=None,
692 ressources=None, chercheurs=None, groupes=None,
693 sites=None, sites_auf=None):
694 self.actualites = actualites
695 self.appels = appels
696 self.evenements = evenements
697 self.ressources = ressources
698 self.chercheurs = chercheurs
699 self.groupes = groupes
700 self.sites = sites
701 self.sites_auf = sites_auf
702
703 def __nonzero__(self):
704 return bool(self.actualites or self.appels or self.evenements or
705 self.ressources or self.chercheurs or self.groupes or
706 self.sites or self.sites_auf)
707
708 class Search(models.Model):
709 user = models.ForeignKey(User, editable=False)
710 content_type = models.ForeignKey(ContentType, editable=False)
711 nom = models.CharField(max_length=100, verbose_name="nom de la recherche")
712 alerte_courriel = models.BooleanField(verbose_name="Envoyer une alerte courriel")
713 derniere_alerte = models.DateField(verbose_name="Date d'envoi de la dernière alerte courriel", null=True, editable=False)
714 q = models.CharField(max_length=100, blank=True, verbose_name="rechercher dans tous les champs")
715 discipline = models.ForeignKey(Discipline, blank=True, null=True)
716 region = models.ForeignKey(Region, blank=True, null=True, verbose_name='région',
717 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.")
718
719 def query_string(self):
720 params = dict()
721 for field in self._meta.fields:
722 if field.name in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
723 continue
724 value = getattr(self, field.column)
725 if value:
726 if isinstance(value, datetime.date):
727 params[field.name] = value.strftime('%d/%m/%Y')
728 else:
729 params[field.name] = smart_str(value)
730 return urlencode(params)
731
732 class Meta:
733 verbose_name = 'recherche transversale'
734 verbose_name_plural = "recherches transversales"
735
736 def __unicode__(self):
737 return self.nom
738
739 def save(self):
740 if self.alerte_courriel:
741 try:
742 original_search = Search.objects.get(id=self.id)
743 if not original_search.alerte_courriel:
744 # On a nouvellement activé l'alerte courriel. Notons la
745 # date.
746 self.derniere_alerte = datetime.date.today() - datetime.timedelta(days=1)
747 except Search.DoesNotExist:
748 self.derniere_alerte = datetime.date.today() - datetime.timedelta(days=1)
749 if (not self.content_type_id):
750 self.content_type = ContentType.objects.get_for_model(self.__class__)
751 super(Search, self).save()
752
753 def as_leaf_class(self):
754 content_type = self.content_type
755 model = content_type.model_class()
756 if(model == Search):
757 return self
758 return model.objects.get(id=self.id)
759
760 def run(self, min_date=None, max_date=None):
761 from chercheurs.models import Chercheur, Groupe
762 from sitotheque.models import Site
763
764 actualites = Actualite.objects
765 evenements = Evenement.objects
766 ressources = Record.objects
767 chercheurs = Chercheur.objects
768 groupes = Groupe.objects
769 sites = Site.objects
770 if self.q:
771 actualites = actualites.search(self.q)
772 evenements = evenements.search(self.q)
773 ressources = ressources.search(self.q)
774 chercheurs = chercheurs.search(self.q)
775 groupes = groupes.search(self.q)
776 sites = sites.search(self.q)
777 if self.discipline:
778 actualites = actualites.filter_discipline(self.discipline)
779 evenements = evenements.filter_discipline(self.discipline)
780 ressources = ressources.filter_discipline(self.discipline)
781 chercheurs = chercheurs.filter_discipline(self.discipline)
782 sites = sites.filter_discipline(self.discipline)
783 if self.region:
784 actualites = actualites.filter_region(self.region)
785 evenements = evenements.filter_region(self.region)
786 ressources = ressources.filter_region(self.region)
787 chercheurs = chercheurs.filter_region(self.region)
788 sites = sites.filter_region(self.region)
789 if min_date:
790 actualites = actualites.filter_date(min=min_date)
791 evenements = evenements.filter_date_modification(min=min_date)
792 ressources = ressources.filter_modified(min=min_date)
793 chercheurs = chercheurs.filter_date_modification(min=min_date)
794 sites = sites.filter_date_maj(min=min_date)
795 if max_date:
796 actualites = actualites.filter_date(max=max_date)
797 evenements = evenements.filter_date_modification(max=max_date)
798 ressources = ressources.filter_modified(max=max_date)
799 chercheurs = chercheurs.filter_date_modification(max=max_date)
800 sites = sites.filter_date_maj(max=max_date)
801
802 try:
803 sites_auf = google_search(0, self.q)['results']
804 except:
805 sites_auf = []
806
807 return GlobalSearchResults(
808 actualites=actualites.order_by('-date').filter_type('actu'),
809 appels=actualites.order_by('-date').filter_type('appels'),
810 evenements=evenements.order_by('-debut'),
811 ressources=ressources.order_by('-id'),
812 chercheurs=chercheurs.order_by('-date_modification'),
813 groupes=groupes.order_by('nom'),
814 sites=sites.order_by('-date_maj'),
815 sites_auf=sites_auf
816 )
817
818 def url(self):
819
820 if self.content_type.model != 'search':
821 obj = self.content_type.get_object_for_this_type(pk=self.pk)
822 return obj.url()
823
824 url = ''
825 if self.discipline:
826 url += '/discipline/%d' % self.discipline.id
827 if self.region:
828 url += '/region/%d' % self.region.id
829 url += '/recherche/'
830 if self.q:
831 url += '?' + urlencode({'q': smart_str(self.q)})
832 return url
833
834 def rss_url(self):
835 return None
836
837 def send_email_alert(self):
838 """Envoie une alerte courriel correspondant à cette recherche"""
839 yesterday = datetime.date.today() - datetime.timedelta(days=1)
840 if self.derniere_alerte is not None:
841 results = self.as_leaf_class().run(min_date=self.derniere_alerte, max_date=yesterday)
842 if results:
843 subject = 'Savoirs en partage - ' + self.nom
844 from_email = CONTACT_EMAIL
845 to_email = self.user.email
846 text_content = u'Voici les derniers résultats correspondant à votre recherche sauvegardée.\n\n'
847 text_content += self.as_leaf_class().get_email_alert_content(results)
848 text_content += u'''
849
850 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
851 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (SITE_ROOT_URL, reverse('recherches'))
852 html_content = '<div style="font-family: Arial, sans-serif">\n' + markdown(smart_str(text_content)) + '</div>\n'
853 msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
854 msg.attach_alternative(html_content, "text/html")
855 msg.send()
856 self.derniere_alerte = yesterday
857 self.save()
858
859 def get_email_alert_content(self, results):
860 content = ''
861 if results.chercheurs:
862 content += u'\n### Nouveaux chercheurs\n\n'
863 for chercheur in results.chercheurs:
864 content += u'- [%s %s](%s%s) \n' % (chercheur.nom.upper(),
865 chercheur.prenom,
866 SITE_ROOT_URL,
867 chercheur.get_absolute_url())
868 content += u' %s\n\n' % chercheur.etablissement_display
869 if results.ressources:
870 content += u'\n### Nouvelles ressources\n\n'
871 for ressource in results.ressources:
872 content += u'- [%s](%s%s)\n\n' % (ressource.title,
873 SITE_ROOT_URL,
874 ressource.get_absolute_url())
875 if ressource.description:
876 content += '\n'
877 content += ''.join([' %s\n' % line for line in textwrap.wrap(ressource.description)])
878 content += '\n'
879
880 if results.actualites:
881 content += u'\n### Nouvelles actualités\n\n'
882 for actualite in results.actualites:
883 content += u'- [%s](%s%s)\n\n' % (actualite.titre,
884 SITE_ROOT_URL,
885 actualite.get_absolute_url())
886 if actualite.texte:
887 content += '\n'
888 content += ''.join([' %s\n' % line for line in textwrap.wrap(actualite.texte)])
889 content += '\n'
890 if results.appels:
891 content += u"\n### Nouveaux appels d'offres\n\n"
892 for appel in results.appels:
893 content += u'- [%s](%s%s)\n\n' % (appel.titre,
894 SITE_ROOT_URL,
895 appel.get_absolute_url())
896 if appel.texte:
897 content += '\n'
898 content += ''.join([' %s\n' % line for line in textwrap.wrap(appel.texte)])
899 content += '\n'
900 if results.evenements:
901 content += u"\n### Nouveaux évènements\n\n"
902 for evenement in results.evenements:
903 content += u'- [%s](%s%s) \n' % (evenement.titre,
904 SITE_ROOT_URL,
905 evenement.get_absolute_url())
906 content += u' où ? : %s \n' % evenement.lieu
907 content += evenement.debut.strftime(u' quand ? : %d/%m/%Y %H:%M \n')
908 content += u' durée ? : %s\n\n' % evenement.duration_display()
909 content += u' quoi ? : '
910 content += '\n '.join(textwrap.wrap(evenement.description))
911 content += '\n\n'
912 if results.sites:
913 content += u"\n### Nouveaux sites\n\n"
914 for site in results.sites:
915 content += u'- [%s](%s%s)\n\n' % (site.titre,
916 SITE_ROOT_URL,
917 site.get_absolute_url())
918 if site.description:
919 content += '\n'
920 content += ''.join([' %s\n' % line for line in textwrap.wrap(site.description)])
921 content += '\n'
922 return content
923
924 class RessourceSearch(Search):
925 auteur = models.CharField(max_length=100, blank=True, verbose_name="auteur ou contributeur")
926 titre = models.CharField(max_length=100, blank=True)
927 sujet = models.CharField(max_length=100, blank=True)
928 publisher = models.CharField(max_length=100, blank=True, verbose_name="éditeur")
929
930 class Meta:
931 verbose_name = 'recherche de ressources'
932 verbose_name_plural = "recherches de ressources"
933
934 def run(self, min_date=None, max_date=None):
935 results = Record.objects
936 if self.q:
937 results = results.search(self.q)
938 if self.auteur:
939 results = results.add_to_query('@(creator,contributor) ' + self.auteur)
940 if self.titre:
941 results = results.add_to_query('@title ' + self.titre)
942 if self.sujet:
943 results = results.add_to_query('@subject ' + self.sujet)
944 if self.publisher:
945 results = results.add_to_query('@publisher ' + self.publisher)
946 if self.discipline:
947 results = results.filter_discipline(self.discipline)
948 if self.region:
949 results = results.filter_region(self.region)
950 if min_date:
951 results = results.filter_modified(min=min_date)
952 if max_date:
953 results = results.filter_modified(max=max_date)
954 if not self.q:
955 """Montrer les résultats les plus récents si on n'a pas fait
956 une recherche par mots-clés."""
957 results = results.order_by('-modified')
958 return results.all()
959
960 def url(self):
961 qs = self.query_string()
962 return reverse('ressources') + ('?' + qs if qs else '')
963
964 def rss_url(self):
965 qs = self.query_string()
966 return reverse('rss_ressources') + ('?' + qs if qs else '')
967
968 def get_email_alert_content(self, results):
969 content = ''
970 for ressource in results:
971 content += u'- [%s](%s%s)\n\n' % (ressource.title,
972 SITE_ROOT_URL,
973 ressource.get_absolute_url())
974 if ressource.description:
975 content += '\n'
976 content += ''.join([' %s\n' % line for line in textwrap.wrap(ressource.description)])
977 content += '\n'
978 return content
979
980 class ActualiteSearchBase(Search):
981 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
982 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
983
984 class Meta:
985 abstract = True
986
987 def run(self, min_date=None, max_date=None):
988 results = Actualite.objects
989 if self.q:
990 results = results.search(self.q)
991 if self.discipline:
992 results = results.filter_discipline(self.discipline)
993 if self.region:
994 results = results.filter_region(self.region)
995 if self.date_min:
996 results = results.filter_date(min=self.date_min)
997 if self.date_max:
998 results = results.filter_date(max=self.date_max)
999 if min_date:
1000 results = results.filter_date(min=min_date)
1001 if max_date:
1002 results = results.filter_date(max=max_date)
1003 return results.all()
1004
1005 def get_email_alert_content(self, results):
1006 content = ''
1007 for actualite in results:
1008 content += u'- [%s](%s%s)\n\n' % (actualite.titre,
1009 SITE_ROOT_URL,
1010 actualite.get_absolute_url())
1011 if actualite.texte:
1012 content += '\n'
1013 content += ''.join([' %s\n' % line for line in textwrap.wrap(actualite.texte)])
1014 content += '\n'
1015 return content
1016
1017 class ActualiteSearch(ActualiteSearchBase):
1018
1019 class Meta:
1020 verbose_name = "recherche d'actualités"
1021 verbose_name_plural = "recherches d'actualités"
1022
1023 def run(self, min_date=None, max_date=None):
1024 return super(ActualiteSearch, self).run(min_date=min_date, max_date=max_date).filter_type('actu')
1025
1026 def url(self):
1027 qs = self.query_string()
1028 return reverse('actualites') + ('?' + qs if qs else '')
1029
1030 def rss_url(self):
1031 qs = self.query_string()
1032 return reverse('rss_actualites') + ('?' + qs if qs else '')
1033
1034 class AppelSearch(ActualiteSearchBase):
1035
1036 class Meta:
1037 verbose_name = "recherche d'appels d'offres"
1038 verbose_name_plural = "recherches d'appels d'offres"
1039
1040 def run(self, min_date=None, max_date=None):
1041 return super(AppelSearch, self).run(min_date=min_date, max_date=max_date).filter_type('appels')
1042
1043 def url(self):
1044 qs = self.query_string()
1045 return reverse('appels') + ('?' + qs if qs else '')
1046
1047 def rss_url(self):
1048 qs = self.query_string()
1049 return reverse('rss_appels') + ('?' + qs if qs else '')
1050
1051 class EvenementSearch(Search):
1052 titre = models.CharField(max_length=100, blank=True, verbose_name="Intitulé")
1053 type = models.CharField(max_length=100, blank=True, choices=Evenement.TYPE_CHOICES)
1054 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
1055 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
1056
1057 class Meta:
1058 verbose_name = "recherche d'évènements"
1059 verbose_name_plural = "recherches d'évènements"
1060
1061 def run(self, min_date=None, max_date=None):
1062 results = Evenement.objects
1063 if self.q:
1064 results = results.search(self.q)
1065 if self.titre:
1066 results = results.add_to_query('@titre ' + self.titre)
1067 if self.discipline:
1068 results = results.filter_discipline(self.discipline)
1069 if self.region:
1070 results = results.filter_region(self.region)
1071 if self.type:
1072 results = results.filter_type(self.type)
1073 if self.date_min:
1074 results = results.filter_debut(min=self.date_min)
1075 if self.date_max:
1076 results = results.filter_debut(max=self.date_max)
1077 if min_date:
1078 results = results.filter_date_modification(min=min_date)
1079 if max_date:
1080 results = results.filter_date_modification(max=max_date)
1081 return results.all()
1082
1083 def url(self):
1084 qs = self.query_string()
1085 return reverse('agenda') + ('?' + qs if qs else '')
1086
1087 def rss_url(self):
1088 qs = self.query_string()
1089 return reverse('rss_agenda') + ('?' + qs if qs else '')
1090
1091 def get_email_alert_content(self, results):
1092 content = ''
1093 for evenement in results:
1094 content += u'- [%s](%s%s) \n' % (evenement.titre,
1095 SITE_ROOT_URL,
1096 evenement.get_absolute_url())
1097 content += u' où ? : %s \n' % evenement.lieu
1098 content += evenement.debut.strftime(u' quand ? : %d/%m/%Y %H:%M \n')
1099 content += u' durée ? : %s\n\n' % evenement.duration_display()
1100 content += u' quoi ? : '
1101 content += '\n '.join(textwrap.wrap(evenement.description))
1102 content += '\n\n'
1103 return content