Permission pour l'ajout de disciplines et régions pour les resources
[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 RecordEdit(Record):
656
657 class Meta:
658 proxy = True
659 verbose_name = 'ressource (édition)'
660 verbose_name_plural = 'ressources (édition)'
661
662 class Serveur(models.Model):
663 """Identification d'un serveur d'ou proviennent les références"""
664 nom = models.CharField(primary_key = True, max_length = 255)
665
666 def __unicode__(self,):
667 return self.nom
668
669 def conf_2_db(self,):
670 for k in RESOURCES.keys():
671 s, created = Serveur.objects.get_or_create(nom=k)
672 s.nom = k
673 s.save()
674
675 class Profile(models.Model):
676 user = models.ForeignKey(User, unique=True)
677 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
678
679 class HarvestLog(models.Model):
680 context = models.CharField(max_length = 255)
681 name = models.CharField(max_length = 255)
682 date = models.DateTimeField(auto_now = True)
683 added = models.IntegerField(null = True, blank = True)
684 updated = models.IntegerField(null = True, blank = True)
685 processed = models.IntegerField(null = True, blank = True)
686 record = models.ForeignKey(Record, null = True, blank = True)
687
688 @staticmethod
689 def add(message):
690 logger = HarvestLog()
691 if message.has_key('record_id'):
692 message['record'] = Record.all_objects.get(id=message['record_id'])
693 del(message['record_id'])
694
695 for k,v in message.items():
696 setattr(logger, k, v)
697 logger.save()
698
699 # Pages statiques
700
701 class PageStatique(models.Model):
702 id = models.CharField(max_length=32, primary_key=True)
703 titre = models.CharField(max_length=100)
704 contenu = models.TextField()
705
706 class Meta:
707 verbose_name_plural = 'pages statiques'
708
709 # Recherches
710
711 class GlobalSearchResults(object):
712
713 def __init__(self, actualites=None, appels=None, evenements=None,
714 ressources=None, chercheurs=None, groupes=None,
715 sites=None, sites_auf=None):
716 self.actualites = actualites
717 self.appels = appels
718 self.evenements = evenements
719 self.ressources = ressources
720 self.chercheurs = chercheurs
721 self.groupes = groupes
722 self.sites = sites
723 self.sites_auf = sites_auf
724
725 def __nonzero__(self):
726 return bool(self.actualites or self.appels or self.evenements or
727 self.ressources or self.chercheurs or self.groupes or
728 self.sites or self.sites_auf)
729
730 class Search(models.Model):
731 user = models.ForeignKey(User, editable=False)
732 content_type = models.ForeignKey(ContentType, editable=False)
733 nom = models.CharField(max_length=100, verbose_name="nom de la recherche")
734 alerte_courriel = models.BooleanField(verbose_name="Envoyer une alerte courriel")
735 derniere_alerte = models.DateField(verbose_name="Date d'envoi de la dernière alerte courriel", null=True, editable=False)
736 q = models.CharField(max_length=100, blank=True, verbose_name="dans tous les champs")
737 discipline = models.ForeignKey(Discipline, blank=True, null=True)
738 region = models.ForeignKey(Region, blank=True, null=True, verbose_name='région',
739 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.")
740
741 def query_string(self):
742 params = dict()
743 for field in self._meta.fields:
744 if field.name in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
745 continue
746 value = getattr(self, field.column)
747 if value:
748 if isinstance(value, datetime.date):
749 params[field.name] = value.strftime('%d/%m/%Y')
750 else:
751 params[field.name] = smart_str(value)
752 return urlencode(params)
753
754 class Meta:
755 verbose_name = 'recherche transversale'
756 verbose_name_plural = "recherches transversales"
757
758 def __unicode__(self):
759 return self.nom
760
761 def save(self):
762 if self.alerte_courriel:
763 try:
764 original_search = Search.objects.get(id=self.id)
765 if not original_search.alerte_courriel:
766 # On a nouvellement activé l'alerte courriel. Notons la
767 # date.
768 self.derniere_alerte = datetime.date.today() - datetime.timedelta(days=1)
769 except Search.DoesNotExist:
770 self.derniere_alerte = datetime.date.today() - datetime.timedelta(days=1)
771 if (not self.content_type_id):
772 self.content_type = ContentType.objects.get_for_model(self.__class__)
773 super(Search, self).save()
774
775 def as_leaf_class(self):
776 content_type = self.content_type
777 model = content_type.model_class()
778 if(model == Search):
779 return self
780 return model.objects.get(id=self.id)
781
782 def run(self, min_date=None, max_date=None):
783 from chercheurs.models import Chercheur, Groupe
784 from sitotheque.models import Site
785
786 actualites = Actualite.objects
787 evenements = Evenement.objects
788 ressources = Record.objects
789 chercheurs = Chercheur.objects
790 groupes = Groupe.objects
791 sites = Site.objects
792 if self.q:
793 actualites = actualites.search(self.q)
794 evenements = evenements.search(self.q)
795 ressources = ressources.search(self.q)
796 chercheurs = chercheurs.search(self.q)
797 groupes = groupes.search(self.q)
798 sites = sites.search(self.q)
799 if self.discipline:
800 actualites = actualites.filter_discipline(self.discipline)
801 evenements = evenements.filter_discipline(self.discipline)
802 ressources = ressources.filter_discipline(self.discipline)
803 chercheurs = chercheurs.filter_discipline(self.discipline)
804 sites = sites.filter_discipline(self.discipline)
805 if self.region:
806 actualites = actualites.filter_region(self.region)
807 evenements = evenements.filter_region(self.region)
808 ressources = ressources.filter_region(self.region)
809 chercheurs = chercheurs.filter_region(self.region)
810 sites = sites.filter_region(self.region)
811 if min_date:
812 actualites = actualites.filter_date(min=min_date)
813 evenements = evenements.filter_date_modification(min=min_date)
814 ressources = ressources.filter_modified(min=min_date)
815 chercheurs = chercheurs.filter_date_modification(min=min_date)
816 sites = sites.filter_date_maj(min=min_date)
817 if max_date:
818 actualites = actualites.filter_date(max=max_date)
819 evenements = evenements.filter_date_modification(max=max_date)
820 ressources = ressources.filter_modified(max=max_date)
821 chercheurs = chercheurs.filter_date_modification(max=max_date)
822 sites = sites.filter_date_maj(max=max_date)
823
824 try:
825 sites_auf = google_search(0, self.q)['results']
826 except:
827 sites_auf = []
828
829 return GlobalSearchResults(
830 actualites=actualites.order_by('-date').filter_type('actu'),
831 appels=actualites.order_by('-date').filter_type('appels'),
832 evenements=evenements.order_by('-debut'),
833 ressources=ressources.order_by('-id'),
834 chercheurs=chercheurs.order_by('-date_modification'),
835 groupes=groupes.order_by('nom'),
836 sites=sites.order_by('-date_maj'),
837 sites_auf=sites_auf
838 )
839
840 def url(self):
841
842 if self.content_type.model != 'search':
843 obj = self.content_type.get_object_for_this_type(pk=self.pk)
844 return obj.url()
845
846 url = ''
847 if self.discipline:
848 url += '/discipline/%d' % self.discipline.id
849 if self.region:
850 url += '/region/%d' % self.region.id
851 url += '/recherche/'
852 if self.q:
853 url += '?' + urlencode({'q': smart_str(self.q)})
854 return url
855
856 def rss_url(self):
857 return None
858
859 def send_email_alert(self):
860 """Envoie une alerte courriel correspondant à cette recherche"""
861 yesterday = datetime.date.today() - datetime.timedelta(days=1)
862 if self.derniere_alerte is not None:
863 results = self.as_leaf_class().run(min_date=self.derniere_alerte, max_date=yesterday)
864 if results:
865 subject = 'Savoirs en partage - ' + self.nom
866 from_email = CONTACT_EMAIL
867 to_email = self.user.email
868 text_content = u'Voici les derniers résultats correspondant à votre recherche sauvegardée.\n\n'
869 text_content += self.as_leaf_class().get_email_alert_content(results)
870 text_content += u'''
871
872 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
873 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (SITE_ROOT_URL, reverse('recherches'))
874 html_content = '<div style="font-family: Arial, sans-serif">\n' + markdown(smart_str(text_content)) + '</div>\n'
875 msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
876 msg.attach_alternative(html_content, "text/html")
877 msg.send()
878 self.derniere_alerte = yesterday
879 self.save()
880
881 def get_email_alert_content(self, results):
882 content = ''
883 if results.chercheurs:
884 content += u'\n### Nouveaux chercheurs\n\n'
885 for chercheur in results.chercheurs:
886 content += u'- [%s %s](%s%s) \n' % (chercheur.nom.upper(),
887 chercheur.prenom,
888 SITE_ROOT_URL,
889 chercheur.get_absolute_url())
890 content += u' %s\n\n' % chercheur.etablissement_display
891 if results.ressources:
892 content += u'\n### Nouvelles ressources\n\n'
893 for ressource in results.ressources:
894 content += u'- [%s](%s%s)\n\n' % (ressource.title,
895 SITE_ROOT_URL,
896 ressource.get_absolute_url())
897 if ressource.description:
898 content += '\n'
899 content += ''.join([' %s\n' % line for line in textwrap.wrap(ressource.description)])
900 content += '\n'
901
902 if results.actualites:
903 content += u'\n### Nouvelles actualités\n\n'
904 for actualite in results.actualites:
905 content += u'- [%s](%s%s)\n\n' % (actualite.titre,
906 SITE_ROOT_URL,
907 actualite.get_absolute_url())
908 if actualite.texte:
909 content += '\n'
910 content += ''.join([' %s\n' % line for line in textwrap.wrap(actualite.texte)])
911 content += '\n'
912 if results.appels:
913 content += u"\n### Nouveaux appels d'offres\n\n"
914 for appel in results.appels:
915 content += u'- [%s](%s%s)\n\n' % (appel.titre,
916 SITE_ROOT_URL,
917 appel.get_absolute_url())
918 if appel.texte:
919 content += '\n'
920 content += ''.join([' %s\n' % line for line in textwrap.wrap(appel.texte)])
921 content += '\n'
922 if results.evenements:
923 content += u"\n### Nouveaux évènements\n\n"
924 for evenement in results.evenements:
925 content += u'- [%s](%s%s) \n' % (evenement.titre,
926 SITE_ROOT_URL,
927 evenement.get_absolute_url())
928 content += u' où ? : %s \n' % evenement.lieu
929 content += evenement.debut.strftime(u' quand ? : %d/%m/%Y %H:%M \n')
930 content += u' durée ? : %s\n\n' % evenement.duration_display()
931 content += u' quoi ? : '
932 content += '\n '.join(textwrap.wrap(evenement.description))
933 content += '\n\n'
934 if results.sites:
935 content += u"\n### Nouveaux sites\n\n"
936 for site in results.sites:
937 content += u'- [%s](%s%s)\n\n' % (site.titre,
938 SITE_ROOT_URL,
939 site.get_absolute_url())
940 if site.description:
941 content += '\n'
942 content += ''.join([' %s\n' % line for line in textwrap.wrap(site.description)])
943 content += '\n'
944 return content
945
946 class RessourceSearch(Search):
947 auteur = models.CharField(max_length=100, blank=True, verbose_name="auteur ou contributeur")
948 titre = models.CharField(max_length=100, blank=True)
949 sujet = models.CharField(max_length=100, blank=True)
950 publisher = models.CharField(max_length=100, blank=True, verbose_name="éditeur")
951 categorie = models.ForeignKey(RecordCategorie, blank=True, null=True, verbose_name='catégorie')
952
953 class Meta:
954 verbose_name = 'recherche de ressources'
955 verbose_name_plural = "recherches de ressources"
956
957 def run(self, min_date=None, max_date=None):
958 results = Record.objects
959 if self.q:
960 results = results.search(self.q)
961 if self.auteur:
962 results = results.add_to_query('@(creator,contributor) ' + self.auteur)
963 if self.titre:
964 results = results.add_to_query('@title ' + self.titre)
965 if self.sujet:
966 results = results.add_to_query('@subject ' + self.sujet)
967 if self.publisher:
968 results = results.add_to_query('@publisher ' + self.publisher)
969 if self.categorie:
970 results = results.add_to_query('@categorie %s' % self.categorie.id)
971 if self.discipline:
972 results = results.filter_discipline(self.discipline)
973 if self.region:
974 results = results.filter_region(self.region)
975 if min_date:
976 results = results.filter_modified(min=min_date)
977 if max_date:
978 results = results.filter_modified(max=max_date)
979 if not self.q:
980 """Montrer les résultats les plus récents si on n'a pas fait
981 une recherche par mots-clés."""
982 results = results.order_by('-modified')
983 return results.all()
984
985 def url(self):
986 qs = self.query_string()
987 return reverse('ressources') + ('?' + qs if qs else '')
988
989 def rss_url(self):
990 qs = self.query_string()
991 return reverse('rss_ressources') + ('?' + qs if qs else '')
992
993 def get_email_alert_content(self, results):
994 content = ''
995 for ressource in results:
996 content += u'- [%s](%s%s)\n\n' % (ressource.title,
997 SITE_ROOT_URL,
998 ressource.get_absolute_url())
999 if ressource.description:
1000 content += '\n'
1001 content += ''.join([' %s\n' % line for line in textwrap.wrap(ressource.description)])
1002 content += '\n'
1003 return content
1004
1005 class ActualiteSearchBase(Search):
1006 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
1007 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
1008
1009 class Meta:
1010 abstract = True
1011
1012 def run(self, min_date=None, max_date=None):
1013 results = Actualite.objects
1014 if self.q:
1015 results = results.search(self.q)
1016 if self.discipline:
1017 results = results.filter_discipline(self.discipline)
1018 if self.region:
1019 results = results.filter_region(self.region)
1020 if self.date_min:
1021 results = results.filter_date(min=self.date_min)
1022 if self.date_max:
1023 results = results.filter_date(max=self.date_max)
1024 if min_date:
1025 results = results.filter_date(min=min_date)
1026 if max_date:
1027 results = results.filter_date(max=max_date)
1028 return results.all()
1029
1030 def get_email_alert_content(self, results):
1031 content = ''
1032 for actualite in results:
1033 content += u'- [%s](%s%s)\n\n' % (actualite.titre,
1034 SITE_ROOT_URL,
1035 actualite.get_absolute_url())
1036 if actualite.texte:
1037 content += '\n'
1038 content += ''.join([' %s\n' % line for line in textwrap.wrap(actualite.texte)])
1039 content += '\n'
1040 return content
1041
1042 class ActualiteSearch(ActualiteSearchBase):
1043
1044 class Meta:
1045 verbose_name = "recherche d'actualités"
1046 verbose_name_plural = "recherches d'actualités"
1047
1048 def run(self, min_date=None, max_date=None):
1049 return super(ActualiteSearch, self).run(min_date=min_date, max_date=max_date).filter_type('actu')
1050
1051 def url(self):
1052 qs = self.query_string()
1053 return reverse('actualites') + ('?' + qs if qs else '')
1054
1055 def rss_url(self):
1056 qs = self.query_string()
1057 return reverse('rss_actualites') + ('?' + qs if qs else '')
1058
1059 class AppelSearch(ActualiteSearchBase):
1060
1061 class Meta:
1062 verbose_name = "recherche d'appels d'offres"
1063 verbose_name_plural = "recherches d'appels d'offres"
1064
1065 def run(self, min_date=None, max_date=None):
1066 return super(AppelSearch, self).run(min_date=min_date, max_date=max_date).filter_type('appels')
1067
1068 def url(self):
1069 qs = self.query_string()
1070 return reverse('appels') + ('?' + qs if qs else '')
1071
1072 def rss_url(self):
1073 qs = self.query_string()
1074 return reverse('rss_appels') + ('?' + qs if qs else '')
1075
1076 class EvenementSearch(Search):
1077 titre = models.CharField(max_length=100, blank=True, verbose_name="Intitulé")
1078 type = models.CharField(max_length=100, blank=True, choices=Evenement.TYPE_CHOICES)
1079 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
1080 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
1081
1082 class Meta:
1083 verbose_name = "recherche d'évènements"
1084 verbose_name_plural = "recherches d'évènements"
1085
1086 def run(self, min_date=None, max_date=None):
1087 results = Evenement.objects
1088 if self.q:
1089 results = results.search(self.q)
1090 if self.titre:
1091 results = results.add_to_query('@titre ' + self.titre)
1092 if self.discipline:
1093 results = results.filter_discipline(self.discipline)
1094 if self.region:
1095 results = results.filter_region(self.region)
1096 if self.type:
1097 results = results.filter_type(self.type)
1098 if self.date_min:
1099 results = results.filter_debut(min=self.date_min)
1100 if self.date_max:
1101 results = results.filter_debut(max=self.date_max)
1102 if min_date:
1103 results = results.filter_date_modification(min=min_date)
1104 if max_date:
1105 results = results.filter_date_modification(max=max_date)
1106 return results.all()
1107
1108 def url(self):
1109 qs = self.query_string()
1110 return reverse('agenda') + ('?' + qs if qs else '')
1111
1112 def rss_url(self):
1113 qs = self.query_string()
1114 return reverse('rss_agenda') + ('?' + qs if qs else '')
1115
1116 def get_email_alert_content(self, results):
1117 content = ''
1118 for evenement in results:
1119 content += u'- [%s](%s%s) \n' % (evenement.titre,
1120 SITE_ROOT_URL,
1121 evenement.get_absolute_url())
1122 content += u' où ? : %s \n' % evenement.lieu
1123 content += evenement.debut.strftime(u' quand ? : %d/%m/%Y %H:%M \n')
1124 content += u' durée ? : %s\n\n' % evenement.duration_display()
1125 content += u' quoi ? : '
1126 content += '\n '.join(textwrap.wrap(evenement.description))
1127 content += '\n\n'
1128 return content