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