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