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