e5d2185aab4ee798308f21141b6a9ed1e7a28ccf
1 # -*- encoding: utf-8 -*-
12 from pytz
.tzinfo
import AmbiguousTimeError
, NonExistentTimeError
13 from urllib
import urlencode
15 from backend_config
import RESOURCES
16 from babel
.dates
import get_timezone_name
17 from caldav
.lib
import error
18 from django
.contrib
.auth
.models
import User
19 from django
.contrib
.contenttypes
.models
import ContentType
20 from django
.core
.mail
import EmailMultiAlternatives
21 from django
.core
.urlresolvers
import reverse
22 from django
.db
import models
23 from django
.db
.models
import Q
, Max
24 from django
.db
.models
.signals
import pre_delete
25 from django
.utils
.encoding
import smart_unicode
, smart_str
26 from djangosphinx
.models
import SphinxQuerySet
, SearchError
27 from markdown2
import markdown
29 from datamaster_modeles
.models
import Region
, Pays
, Thematique
30 from savoirs
.globals import META
31 from settings
import CALENDRIER_URL
, SITE_ROOT_URL
, CONTACT_EMAIL
33 # Fonctionnalités communes à tous les query sets
35 class RandomQuerySetMixin(object):
36 """Mixin pour les modèles.
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.
42 def random(self
, n
=1):
43 """Récupère aléatoirement un nombre donné d'objets."""
45 positions
= random
.sample(xrange(count
), min(n
, count
))
46 return [self
[p
] for p
in positions
]
48 class SEPQuerySet(models
.query
.QuerySet
, RandomQuerySetMixin
):
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``."""
55 qs
= qs
.filter(**{field
+ '__gte': min})
57 qs
= qs
.filter(**{field
+ '__lt': max + datetime
.timedelta(days
=1)})
60 class SEPSphinxQuerySet(SphinxQuerySet
, RandomQuerySetMixin
):
61 """Fonctionnalités communes aux query sets de Sphinx."""
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',
69 def add_to_query(self
, query
):
70 """Ajoute une partie à la requête texte."""
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...)
76 query
= query
[:i
] + query
[i
+1:]
78 new_query
= smart_unicode(self
._query
) + ' ' + query
if self
._query
else query
79 return self
.query(new_query
)
81 def search(self
, text
):
82 """Recherche ``text`` dans tous les champs."""
83 return self
.add_to_query('@* ' + text
)
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
)
90 def filter_region(self
, region
):
91 """Par défaut, le filtre par région cherche le nom de la région dans
93 return self
.search('"%s"' % region
.nom
)
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``."""
100 qs
= qs
.filter(**{field
+ '__gte': min.toordinal()+365})
102 qs
= qs
.filter(**{field
+ '__lte': max.toordinal()+365})
105 def _get_sphinx_results(self
):
107 return SphinxQuerySet
._get_sphinx_results(self
)
109 # Essayons d'enlever les caractères qui peuvent poser problème.
110 for c
in '|!@()~/<=^$':
111 self
._query
= self
._query
.replace(c
, ' ')
113 return SphinxQuerySet
._get_sphinx_results(self
)
115 # Ça ne marche toujours pas. Enlevons les guillemets et les
118 self
._query
= self
._query
.replace(c
, ' ')
119 return SphinxQuerySet
._get_sphinx_results(self
)
121 class SEPManager(models
.Manager
):
122 """Lorsque les méthodes ``search``, ``filter_region`` et
123 ``filter_discipline`` sont appelées sur ce manager, le query set
124 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
126 def query(self
, query
):
127 return self
.get_sphinx_query_set().query(query
)
129 def add_to_query(self
, query
):
130 return self
.get_sphinx_query_set().add_to_query(query
)
132 def search(self
, text
):
133 return self
.get_sphinx_query_set().search(text
)
135 def filter_region(self
, region
):
136 return self
.get_sphinx_query_set().filter_region(region
)
138 def filter_discipline(self
, discipline
):
139 return self
.get_sphinx_query_set().filter_discipline(discipline
)
143 class Discipline(models
.Model
):
144 id = models
.IntegerField(primary_key
=True, db_column
='id_discipline')
145 nom
= models
.CharField(max_length
=765, db_column
='nom_discipline')
147 def __unicode__ (self
):
151 db_table
= u
'discipline'
156 class SourceActualite(models
.Model
):
158 ('actu', 'Actualités'),
159 ('appels', "Appels d'offres"),
162 nom
= models
.CharField(max_length
=255)
163 url
= models
.CharField(max_length
=255, verbose_name
='URL', blank
=True)
164 type = models
.CharField(max_length
=10, default
='actu', choices
=TYPE_CHOICES
)
167 verbose_name
= u
'fil RSS syndiqué'
168 verbose_name_plural
= u
'fils RSS syndiqués'
170 def __unicode__(self
,):
171 return u
"%s (%s)" % (self
.nom
, self
.get_type_display())
174 """Mise à jour du fil RSS."""
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
)
186 class ActualiteQuerySet(SEPQuerySet
):
188 def filter_date(self
, min=None, max=None):
189 return self
._filter_date('date', min=min, max=max)
191 def filter_type(self
, type):
192 return self
.filter(source__type
=type)
194 class ActualiteSphinxQuerySet(SEPSphinxQuerySet
):
196 def __init__(self
, model
=None):
197 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_actualites',
198 weights
=dict(titre
=3))
200 def filter_date(self
, min=None, max=None):
201 return self
._filter_date('date', min=min, max=max)
203 TYPE_CODES
= {'actu': 1, 'appels': 2}
204 def filter_type(self
, type):
205 return self
.filter(type=self
.TYPE_CODES
[type])
207 class ActualiteManager(SEPManager
):
209 def get_query_set(self
):
210 return ActualiteQuerySet(self
.model
).filter(visible
=True)
212 def get_sphinx_query_set(self
):
213 return ActualiteSphinxQuerySet(self
.model
).order_by('-date')
215 def filter_date(self
, min=None, max=None):
216 return self
.get_query_set().filter_date(min=min, max=max)
218 def filter_type(self
, type):
219 return self
.get_query_set().filter_type(type)
221 class Actualite(models
.Model
):
222 id = models
.AutoField(primary_key
=True, db_column
='id_actualite')
223 titre
= models
.CharField(max_length
=765, db_column
='titre_actualite')
224 texte
= models
.TextField(db_column
='texte_actualite')
225 url
= models
.CharField(max_length
=765, db_column
='url_actualite')
226 date
= models
.DateField(db_column
='date_actualite')
227 visible
= models
.BooleanField(db_column
='visible_actualite', default
=False)
228 ancienid
= models
.IntegerField(db_column
='ancienId_actualite', blank
=True, null
=True)
229 source
= models
.ForeignKey(SourceActualite
, related_name
='actualites')
230 disciplines
= models
.ManyToManyField(Discipline
, blank
=True, related_name
="actualites")
231 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="actualites", verbose_name
='régions')
233 objects
= ActualiteManager()
234 all_objects
= models
.Manager()
237 db_table
= u
'actualite'
240 def __unicode__ (self
):
241 return "%s" % (self
.titre
)
243 def get_absolute_url(self
):
244 return reverse('actualite', kwargs
={'id': self
.id})
246 def assigner_disciplines(self
, disciplines
):
247 self
.disciplines
.add(*disciplines
)
249 def assigner_regions(self
, regions
):
250 self
.regions
.add(*regions
)
254 class EvenementQuerySet(SEPQuerySet
):
256 def filter_type(self
, type):
257 return self
.filter(type=type)
259 def filter_debut(self
, min=None, max=None):
260 return self
._filter_date('debut', min=min, max=max)
262 def filter_date_modification(self
, min=None, max=None):
263 return self
._filter_date('date_modification', min=min, max=max)
265 class EvenementSphinxQuerySet(SEPSphinxQuerySet
):
267 def __init__(self
, model
=None):
268 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_evenements',
269 weights
=dict(titre
=3))
271 def filter_type(self
, type):
272 return self
.add_to_query('@type "%s"' % type)
274 def filter_debut(self
, min=None, max=None):
275 return self
._filter_date('debut', min=min, max=max)
277 def filter_date_modification(self
, min=None, max=None):
278 return self
._filter_date('date_modification', min=min, max=max)
280 class EvenementManager(SEPManager
):
282 def get_query_set(self
):
283 return EvenementQuerySet(self
.model
).filter(approuve
=True)
285 def get_sphinx_query_set(self
):
286 return EvenementSphinxQuerySet(self
.model
).order_by('-debut')
288 def filter_type(self
, type):
289 return self
.get_query_set().filter_type(type)
291 def filter_debut(self
, min=None, max=None):
292 return self
.get_query_set().filter_debut(min=min, max=max)
294 def filter_date_modification(self
, min=None, max=None):
295 return self
.get_query_set().filter_date_modification(min=min, max=max)
297 def build_time_zone_choices(pays
=None):
298 timezones
= pytz
.country_timezones
[pays
] if pays
else pytz
.common_timezones
300 now
= datetime
.datetime
.now()
301 for tzname
in timezones
:
302 tz
= pytz
.timezone(tzname
)
303 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
305 offset
= tz
.utcoffset(now
)
306 except (AmbiguousTimeError
, NonExistentTimeError
):
307 # oups. On est en train de changer d'heure. Ça devrait être fini
309 offset
= tz
.utcoffset(now
+ datetime
.timedelta(days
=1))
310 seconds
= offset
.seconds
+ offset
.days
* 86400
311 (hours
, minutes
) = divmod(seconds
// 60, 60)
312 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) if minutes
else 'UTC%+d' % hours
313 result
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
315 return [(x
[1], x
[2]) for x
in result
]
317 class Evenement(models
.Model
):
318 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
319 (u
'Conférence', u
'Conférence'),
320 (u
'Appel à contribution', u
'Appel à contribution'),
321 (u
'Journée d\'étude', u
'Journée d\'étude'),
322 (u
'Autre', u
'Autre'))
323 TIME_ZONE_CHOICES
= build_time_zone_choices()
325 uid
= models
.CharField(max_length
=255, default
=str(uuid
.uuid1()))
326 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
327 titre
= models
.CharField(max_length
=255)
328 discipline
= models
.ForeignKey('Discipline', related_name
= "discipline",
329 blank
= True, null
= True)
330 discipline_secondaire
= models
.ForeignKey('Discipline', related_name
="discipline_secondaire",
331 verbose_name
=u
"discipline secondaire",
332 blank
=True, null
=True)
333 mots_cles
= models
.TextField('Mots-Clés', blank
=True, null
=True)
334 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
335 adresse
= models
.TextField()
336 ville
= models
.CharField(max_length
=100)
337 pays
= models
.ForeignKey(Pays
, null
=True, related_name
='evenements')
338 debut
= models
.DateTimeField(default
=datetime
.datetime
.now
)
339 fin
= models
.DateTimeField(default
=datetime
.datetime
.now
)
340 fuseau
= models
.CharField(max_length
=100, choices
=TIME_ZONE_CHOICES
, verbose_name
='fuseau horaire')
341 description
= models
.TextField()
342 contact
= models
.TextField(null
=True) # champ obsolète
343 prenom
= models
.CharField('prénom', max_length
=100)
344 nom
= models
.CharField(max_length
=100)
345 courriel
= models
.EmailField()
346 url
= models
.CharField(max_length
=255, blank
=True, null
=True)
347 piece_jointe
= models
.FileField(upload_to
='agenda/pj', blank
=True, verbose_name
='pièce jointe')
348 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="evenements", verbose_name
='régions additionnelles',
349 help_text
="On considère d'emblée que l'évènement se déroule dans la région "
350 "dans laquelle se trouve le pays indiqué plus haut. Il est possible "
351 "de désigner ici des régions additionnelles.")
352 date_modification
= models
.DateTimeField(editable
=False, auto_now
=True, null
=True)
354 objects
= EvenementManager()
355 all_objects
= models
.Manager()
358 ordering
= ['-debut']
359 verbose_name
= u
'évènement'
360 verbose_name_plural
= u
'évènements'
362 def __unicode__(self
):
363 return "[%s] %s" % (self
.uid
, self
.titre
)
365 def get_absolute_url(self
):
366 return reverse('evenement', kwargs
={'id': self
.id})
368 def duration_display(self
):
369 delta
= self
.fin
- self
.debut
370 minutes
, seconds
= divmod(delta
.seconds
, 60)
371 hours
, minutes
= divmod(minutes
, 60)
375 parts
.append('1 jour')
377 parts
.append('%d jours' % days
)
379 parts
.append('1 heure')
381 parts
.append('%d heures' % hours
)
383 parts
.append('1 minute')
385 parts
.append('%d minutes' % minutes
)
386 return ' '.join(parts
)
388 def piece_jointe_display(self
):
389 return self
.piece_jointe
and os
.path
.basename(self
.piece_jointe
.name
)
391 def courriel_display(self
):
392 return self
.courriel
.replace(u
'@', u
' (à) ')
398 bits
.append(self
.adresse
)
400 bits
.append(self
.ville
)
402 bits
.append(self
.pays
.nom
)
403 return ', '.join(bits
)
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')
410 def save(self
, *args
, **kwargs
):
411 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
413 self
.contact
= '' # Vider ce champ obsolète à la première occasion...
415 super(Evenement
, self
).save(*args
, **kwargs
)
418 # methodes de commnunications avec CALDAV
420 """Retourne l'evenement django sous forme d'objet icalendar"""
421 cal
= vobject
.iCalendar()
424 # fournit son propre uid
425 if self
.uid
in [None, ""]:
426 self
.uid
= str(uuid
.uuid1())
428 cal
.vevent
.add('uid').value
= self
.uid
430 cal
.vevent
.add('summary').value
= self
.titre
432 if self
.mots_cles
is None:
435 kw
= self
.mots_cles
.split(",")
438 kw
.append(self
.discipline
.nom
)
439 kw
.append(self
.discipline_secondaire
.nom
)
443 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
445 cal
.vevent
.add('x-auf-keywords').value
= k
447 description
= self
.description
449 if len(self
.description
) > 0:
451 description
+= u
"Mots-clés: " + ", ".join(kw
)
453 cal
.vevent
.add('dtstart').value
= combine(self
.debut
, pytz
.timezone(self
.fuseau
))
454 cal
.vevent
.add('dtend').value
= combine(self
.fin
, pytz
.timezone(self
.fuseau
))
455 cal
.vevent
.add('created').value
= combine(datetime
.datetime
.now(), "UTC")
456 cal
.vevent
.add('dtstamp').value
= combine(datetime
.datetime
.now(), "UTC")
457 if len(description
) > 0:
458 cal
.vevent
.add('description').value
= description
459 if len(self
.contact
) > 0:
460 cal
.vevent
.add('contact').value
= self
.contact
461 if len(self
.url
) > 0:
462 cal
.vevent
.add('url').value
= self
.url
463 cal
.vevent
.add('location').value
= ', '.join([x
for x
in [self
.adresse
, self
.ville
, self
.pays
.nom
] if x
])
464 if self
.piece_jointe
:
465 url
= self
.piece_jointe
.url
466 if not url
.startswith('http://'):
467 url
= SITE_ROOT_URL
+ url
468 cal
.vevent
.add('attach').value
= url
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é"""
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
)
482 self
.approuve
= False
484 def delete_vevent(self
,):
485 """Supprime l'evenement sur le serveur caldav"""
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
)
493 except error
.NotFoundError
:
496 def assigner_regions(self
, regions
):
497 self
.regions
.add(*regions
)
499 def assigner_disciplines(self
, disciplines
):
500 if len(disciplines
) == 1:
502 self
.discipline_secondaire
= disciplines
[0]
504 self
.discipline
= disciplines
[0]
505 elif len(disciplines
) >= 2:
506 self
.discipline
= disciplines
[0]
507 self
.discipline_secondaire
= disciplines
[1]
509 def delete_vevent(sender
, instance
, *args
, **kwargs
):
510 # Surcharge du comportement de suppression
511 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
512 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
513 instance
.delete_vevent()
514 pre_delete
.connect(delete_vevent
, sender
=Evenement
)
518 class ListSet(models
.Model
):
519 spec
= models
.CharField(primary_key
= True, max_length
= 255)
520 name
= models
.CharField(max_length
= 255)
521 server
= models
.CharField(max_length
= 255)
522 validated
= models
.BooleanField(default
= True)
524 def __unicode__(self
,):
527 class RecordCategorie(models
.Model
):
528 nom
= models
.CharField(max_length
=255)
531 verbose_name
= 'catégorie ressource'
532 verbose_name_plural
= 'catégories ressources'
534 def __unicode__(self
):
538 class RecordQuerySet(SEPQuerySet
):
540 def filter_modified(self
, min=None, max=None):
541 return self
._filter_date('modified', min=min, max=max)
543 class RecordSphinxQuerySet(SEPSphinxQuerySet
):
545 def __init__(self
, model
=None):
546 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_ressources',
547 weights
=dict(title
=3))
549 def filter_modified(self
, min=None, max=None):
550 return self
._filter_date('modified', min=min, max=max)
552 class RecordManager(SEPManager
):
554 def get_query_set(self
):
555 """Ne garder que les ressources validées et qui sont soit dans aucun
556 listset ou au moins dans un listset validé."""
557 qs
= RecordQuerySet(self
.model
)
558 qs
= qs
.filter(validated
=True)
559 qs
= qs
.filter(Q(listsets__isnull
=True) |
Q(listsets__validated
=True))
562 def get_sphinx_query_set(self
):
563 return RecordSphinxQuerySet(self
.model
)
565 def filter_modified(self
, min=None, max=None):
566 return self
.get_query_set().filter_modified(min=min, max=max)
568 class Record(models
.Model
):
570 #fonctionnement interne
571 id = models
.AutoField(primary_key
= True)
572 server
= models
.CharField(max_length
= 255, verbose_name
=u
'serveur')
573 last_update
= models
.CharField(max_length
= 255)
574 last_checksum
= models
.CharField(max_length
= 255)
575 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
578 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
579 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
580 description
= models
.TextField(null
=True, blank
=True)
581 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
582 identifier
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
583 uri
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
584 source
= models
.TextField(null
= True, blank
= True)
585 contributor
= models
.TextField(null
= True, blank
= True)
586 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
587 publisher
= models
.TextField(null
= True, blank
= True)
588 type = models
.TextField(null
= True, blank
= True)
589 format
= models
.TextField(null
= True, blank
= True)
590 language
= models
.TextField(null
= True, blank
= True)
592 listsets
= models
.ManyToManyField(ListSet
, null
= True, blank
= True)
594 #SEP 2 (aucune données récoltées)
595 alt_title
= models
.TextField(null
= True, blank
= True)
596 abstract
= models
.TextField(null
= True, blank
= True)
597 creation
= models
.CharField(max_length
= 255, null
= True, blank
= True)
598 issued
= models
.CharField(max_length
= 255, null
= True, blank
= True)
599 isbn
= models
.TextField(null
= True, blank
= True)
600 orig_lang
= models
.TextField(null
= True, blank
= True)
602 categorie
= models
.ForeignKey(RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie')
604 # Metadata AUF multivaluées
605 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
606 thematiques
= models
.ManyToManyField(Thematique
, blank
=True, verbose_name
='thématiques')
607 pays
= models
.ManyToManyField(Pays
, blank
=True)
608 regions
= models
.ManyToManyField(Region
, blank
=True, verbose_name
='régions')
611 objects
= RecordManager()
612 all_objects
= models
.Manager()
615 verbose_name
= 'ressource'
617 def __unicode__(self
):
618 return "[%s] %s" % (self
.server
, self
.title
)
620 def get_absolute_url(self
):
621 return reverse('ressource', kwargs
={'id': self
.id})
623 def getServeurURL(self
):
624 """Retourne l'URL du serveur de provenance"""
625 return RESOURCES
[self
.server
]['url']
627 def est_complet(self
):
628 """teste si le record à toutes les données obligatoires"""
629 return self
.disciplines
.count() > 0 and \
630 self
.thematiques
.count() > 0 and \
631 self
.pays
.count() > 0 and \
632 self
.regions
.count() > 0
634 def assigner_regions(self
, regions
):
635 self
.regions
.add(*regions
)
637 def assigner_disciplines(self
, disciplines
):
638 self
.disciplines
.add(*disciplines
)
640 class Serveur(models
.Model
):
641 """Identification d'un serveur d'ou proviennent les références"""
642 nom
= models
.CharField(primary_key
= True, max_length
= 255)
644 def __unicode__(self
,):
647 def conf_2_db(self
,):
648 for k
in RESOURCES
.keys():
649 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
653 class Profile(models
.Model
):
654 user
= models
.ForeignKey(User
, unique
=True)
655 serveurs
= models
.ManyToManyField(Serveur
, null
= True, blank
= True)
657 class HarvestLog(models
.Model
):
658 context
= models
.CharField(max_length
= 255)
659 name
= models
.CharField(max_length
= 255)
660 date
= models
.DateTimeField(auto_now
= True)
661 added
= models
.IntegerField(null
= True, blank
= True)
662 updated
= models
.IntegerField(null
= True, blank
= True)
663 processed
= models
.IntegerField(null
= True, blank
= True)
664 record
= models
.ForeignKey(Record
, null
= True, blank
= True)
668 logger
= HarvestLog()
669 if message
.has_key('record_id'):
670 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
671 del(message
['record_id'])
673 for k
,v
in message
.items():
674 setattr(logger
, k
, v
)
679 class PageStatique(models
.Model
):
680 id = models
.CharField(max_length
=32, primary_key
=True)
681 titre
= models
.CharField(max_length
=100)
682 contenu
= models
.TextField()
685 verbose_name_plural
= 'pages statiques'
689 class GlobalSearchResults(object):
691 def __init__(self
, actualites
=None, appels
=None, evenements
=None,
692 ressources
=None, chercheurs
=None, groupes
=None,
693 sites
=None, sites_auf
=None):
694 self
.actualites
= actualites
696 self
.evenements
= evenements
697 self
.ressources
= ressources
698 self
.chercheurs
= chercheurs
699 self
.groupes
= groupes
701 self
.sites_auf
= sites_auf
703 def __nonzero__(self
):
704 return bool(self
.actualites
or self
.appels
or self
.evenements
or
705 self
.ressources
or self
.chercheurs
or self
.groupes
or
706 self
.sites
or self
.sites_auf
)
708 class Search(models
.Model
):
709 user
= models
.ForeignKey(User
, editable
=False)
710 content_type
= models
.ForeignKey(ContentType
, editable
=False)
711 nom
= models
.CharField(max_length
=100, verbose_name
="nom de la recherche")
712 alerte_courriel
= models
.BooleanField(verbose_name
="Envoyer une alerte courriel")
713 derniere_alerte
= models
.DateField(verbose_name
="Date d'envoi de la dernière alerte courriel", null
=True, editable
=False)
714 q
= models
.CharField(max_length
=100, blank
=True, verbose_name
="rechercher dans tous les champs")
715 discipline
= models
.ForeignKey(Discipline
, blank
=True, null
=True)
716 region
= models
.ForeignKey(Region
, blank
=True, null
=True, verbose_name
='région',
717 help_text
="La région est ici définie au sens, non strictement géographique, du Bureau régional de l'AUF de référence.")
719 def query_string(self
):
721 for field
in self
._meta
.fields
:
722 if field
.name
in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
724 value
= getattr(self
, field
.column
)
726 if isinstance(value
, datetime
.date
):
727 params
[field
.name
] = value
.strftime('%d/%m/%Y')
729 params
[field
.name
] = smart_str(value
)
730 return urlencode(params
)
733 verbose_name
= 'recherche transversale'
734 verbose_name_plural
= "recherches transversales"
737 if self
.alerte_courriel
:
739 original_search
= Search
.objects
.get(id=self
.id)
740 if not original_search
.alerte_courriel
:
741 # On a nouvellement activé l'alerte courriel. Notons la
743 self
.derniere_alerte
= datetime
.date
.today() - datetime
.timedelta(days
=1)
744 except Search
.DoesNotExist
:
745 self
.derniere_alerte
= datetime
.date
.today() - datetime
.timedelta(days
=1)
746 if (not self
.content_type_id
):
747 self
.content_type
= ContentType
.objects
.get_for_model(self
.__class__
)
748 super(Search
, self
).save()
750 def as_leaf_class(self
):
751 content_type
= self
.content_type
752 model
= content_type
.model_class()
755 return model
.objects
.get(id=self
.id)
757 def run(self
, min_date
=None, max_date
=None):
758 from chercheurs
.models
import Chercheur
, Groupe
759 from sitotheque
.models
import Site
761 actualites
= Actualite
.objects
762 evenements
= Evenement
.objects
763 ressources
= Record
.objects
764 chercheurs
= Chercheur
.objects
765 groupes
= Groupe
.objects
768 actualites
= actualites
.search(self
.q
)
769 evenements
= evenements
.search(self
.q
)
770 ressources
= ressources
.search(self
.q
)
771 chercheurs
= chercheurs
.search(self
.q
)
772 groupes
= groupes
.search(self
.q
)
773 sites
= sites
.search(self
.q
)
775 actualites
= actualites
.filter_discipline(self
.discipline
)
776 evenements
= evenements
.filter_discipline(self
.discipline
)
777 ressources
= ressources
.filter_discipline(self
.discipline
)
778 chercheurs
= chercheurs
.filter_discipline(self
.discipline
)
779 sites
= sites
.filter_discipline(self
.discipline
)
781 actualites
= actualites
.filter_region(self
.region
)
782 evenements
= evenements
.filter_region(self
.region
)
783 ressources
= ressources
.filter_region(self
.region
)
784 chercheurs
= chercheurs
.filter_region(self
.region
)
785 sites
= sites
.filter_region(self
.region
)
787 actualites
= actualites
.filter_date(min=min_date
)
788 evenements
= evenements
.filter_date_modification(min=min_date
)
789 ressources
= ressources
.filter_modified(min=min_date
)
790 chercheurs
= chercheurs
.filter_date_modification(min=min_date
)
791 sites
= sites
.filter_date_maj(min=min_date
)
793 actualites
= actualites
.filter_date(max=max_date
)
794 evenements
= evenements
.filter_date_modification(max=max_date
)
795 ressources
= ressources
.filter_modified(max=max_date
)
796 chercheurs
= chercheurs
.filter_date_modification(max=max_date
)
797 sites
= sites
.filter_date_maj(max=max_date
)
800 sites_auf
= google_search(0, self
.q
)['results']
804 return GlobalSearchResults(
805 actualites
=actualites
.order_by('-date').filter_type('actu'),
806 appels
=actualites
.order_by('-date').filter_type('appels'),
807 evenements
=evenements
.order_by('-debut'),
808 ressources
=ressources
.order_by('-id'),
809 chercheurs
=chercheurs
.order_by('-date_modification'),
810 groupes
=groupes
.order_by('nom'),
811 sites
=sites
.order_by('-date_maj'),
818 url
+= '/discipline/%d' % self
.discipline
.id
820 url
+= '/region/%d' % self
.region
.id
823 url
+= '?' + urlencode({'q': smart_str(self
.q
)})
829 def send_email_alert(self
):
830 """Envoie une alerte courriel correspondant à cette recherche"""
831 yesterday
= datetime
.date
.today() - datetime
.timedelta(days
=1)
832 if self
.derniere_alerte
is not None:
833 results
= self
.as_leaf_class().run(min_date
=self
.derniere_alerte
, max_date
=yesterday
)
835 subject
= 'Savoirs en partage - ' + self
.nom
836 from_email
= CONTACT_EMAIL
837 to_email
= self
.user
.email
838 text_content
= u
'Voici les derniers résultats correspondant à votre recherche sauvegardée.\n\n'
839 text_content
+= self
.as_leaf_class().get_email_alert_content(results
)
842 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
843 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (SITE_ROOT_URL
, reverse('recherches'))
844 html_content
= '<div style="font-family: Arial, sans-serif">\n' + markdown(smart_str(text_content
)) + '</div>\n'
845 msg
= EmailMultiAlternatives(subject
, text_content
, from_email
, [to_email
])
846 msg
.attach_alternative(html_content
, "text/html")
848 self
.derniere_alerte
= yesterday
851 def get_email_alert_content(self
, results
):
853 if results
.chercheurs
:
854 content
+= u
'\n### Nouveaux chercheurs\n\n'
855 for chercheur
in results
.chercheurs
:
856 content
+= u
'- [%s %s](%s%s) \n' % (chercheur
.nom
.upper(),
859 chercheur
.get_absolute_url())
860 content
+= u
' %s\n\n' % chercheur
.etablissement_display
861 if results
.ressources
:
862 content
+= u
'\n### Nouvelles ressources\n\n'
863 for ressource
in results
.ressources
:
864 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
866 ressource
.get_absolute_url())
867 if ressource
.description
:
869 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(ressource
.description
)])
872 if results
.actualites
:
873 content
+= u
'\n### Nouvelles actualités\n\n'
874 for actualite
in results
.actualites
:
875 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
877 actualite
.get_absolute_url())
880 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(actualite
.texte
)])
883 content
+= u
"\n### Nouveaux appels d'offres\n\n"
884 for appel
in results
.appels
:
885 content
+= u
'- [%s](%s%s)\n\n' % (appel
.titre
,
887 appel
.get_absolute_url())
890 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(appel
.texte
)])
892 if results
.evenements
:
893 content
+= u
"\n### Nouveaux évènements\n\n"
894 for evenement
in results
.evenements
:
895 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
897 evenement
.get_absolute_url())
898 content
+= u
' où ? : %s \n' % evenement
.lieu
899 content
+= evenement
.debut
.strftime(u
' quand ? : %d/%m/%Y %H:%M \n')
900 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
901 content
+= u
' quoi ? : '
902 content
+= '\n '.join(textwrap
.wrap(evenement
.description
))
905 content
+= u
"\n### Nouveaux sites\n\n"
906 for site
in results
.sites
:
907 content
+= u
'- [%s](%s%s)\n\n' % (site
.titre
,
909 site
.get_absolute_url())
912 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(site
.description
)])
916 class RessourceSearch(Search
):
917 auteur
= models
.CharField(max_length
=100, blank
=True, verbose_name
="auteur ou contributeur")
918 titre
= models
.CharField(max_length
=100, blank
=True)
919 sujet
= models
.CharField(max_length
=100, blank
=True)
920 publisher
= models
.CharField(max_length
=100, blank
=True, verbose_name
="éditeur")
923 verbose_name
= 'recherche de ressources'
924 verbose_name_plural
= "recherches de ressources"
926 def run(self
, min_date
=None, max_date
=None):
927 results
= Record
.objects
929 results
= results
.search(self
.q
)
931 results
= results
.add_to_query('@(creator,contributor) ' + self
.auteur
)
933 results
= results
.add_to_query('@title ' + self
.titre
)
935 results
= results
.add_to_query('@subject ' + self
.sujet
)
937 results
= results
.add_to_query('@publisher ' + self
.publisher
)
939 results
= results
.filter_discipline(self
.discipline
)
941 results
= results
.filter_region(self
.region
)
943 results
= results
.filter_modified(min=min_date
)
945 results
= results
.filter_modified(max=max_date
)
947 """Montrer les résultats les plus récents si on n'a pas fait
948 une recherche par mots-clés."""
949 results
= results
.order_by('-modified')
953 qs
= self
.query_string()
954 return reverse('ressources') + ('?' + qs
if qs
else '')
957 qs
= self
.query_string()
958 return reverse('rss_ressources') + ('?' + qs
if qs
else '')
960 def get_email_alert_content(self
, results
):
962 for ressource
in results
:
963 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
965 ressource
.get_absolute_url())
966 if ressource
.description
:
968 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(ressource
.description
)])
972 class ActualiteSearchBase(Search
):
973 date_min
= models
.DateField(blank
=True, null
=True, verbose_name
="depuis le")
974 date_max
= models
.DateField(blank
=True, null
=True, verbose_name
="jusqu'au")
979 def run(self
, min_date
=None, max_date
=None):
980 results
= Actualite
.objects
982 results
= results
.search(self
.q
)
984 results
= results
.filter_discipline(self
.discipline
)
986 results
= results
.filter_region(self
.region
)
988 results
= results
.filter_date(min=self
.date_min
)
990 results
= results
.filter_date(max=self
.date_max
)
992 results
= results
.filter_date(min=min_date
)
994 results
= results
.filter_date(max=max_date
)
997 def get_email_alert_content(self
, results
):
999 for actualite
in results
:
1000 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
1002 actualite
.get_absolute_url())
1005 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(actualite
.texte
)])
1009 class ActualiteSearch(ActualiteSearchBase
):
1012 verbose_name
= "recherche d'actualités"
1013 verbose_name_plural
= "recherches d'actualités"
1015 def run(self
, min_date
=None, max_date
=None):
1016 return super(ActualiteSearch
, self
).run(min_date
=min_date
, max_date
=max_date
).filter_type('actu')
1019 qs
= self
.query_string()
1020 return reverse('actualites') + ('?' + qs
if qs
else '')
1023 qs
= self
.query_string()
1024 return reverse('rss_actualites') + ('?' + qs
if qs
else '')
1026 class AppelSearch(ActualiteSearchBase
):
1029 verbose_name
= "recherche d'appels d'offres"
1030 verbose_name_plural
= "recherches d'appels d'offres"
1032 def run(self
, min_date
=None, max_date
=None):
1033 return super(AppelSearch
, self
).run(min_date
=min_date
, max_date
=max_date
).filter_type('appels')
1036 qs
= self
.query_string()
1037 return reverse('appels') + ('?' + qs
if qs
else '')
1040 qs
= self
.query_string()
1041 return reverse('rss_appels') + ('?' + qs
if qs
else '')
1043 class EvenementSearch(Search
):
1044 titre
= models
.CharField(max_length
=100, blank
=True, verbose_name
="Intitulé")
1045 type = models
.CharField(max_length
=100, blank
=True, choices
=Evenement
.TYPE_CHOICES
)
1046 date_min
= models
.DateField(blank
=True, null
=True, verbose_name
="depuis le")
1047 date_max
= models
.DateField(blank
=True, null
=True, verbose_name
="jusqu'au")
1050 verbose_name
= "recherche d'évènements"
1051 verbose_name_plural
= "recherches d'évènements"
1053 def run(self
, min_date
=None, max_date
=None):
1054 results
= Evenement
.objects
1056 results
= results
.search(self
.q
)
1058 results
= results
.add_to_query('@titre ' + self
.titre
)
1060 results
= results
.filter_discipline(self
.discipline
)
1062 results
= results
.filter_region(self
.region
)
1064 results
= results
.filter_type(self
.type)
1066 results
= results
.filter_debut(min=self
.date_min
)
1068 results
= results
.filter_debut(max=self
.date_max
)
1070 results
= results
.filter_date_modification(min=min_date
)
1072 results
= results
.filter_date_modification(max=max_date
)
1073 return results
.all()
1076 qs
= self
.query_string()
1077 return reverse('agenda') + ('?' + qs
if qs
else '')
1080 qs
= self
.query_string()
1081 return reverse('rss_agenda') + ('?' + qs
if qs
else '')
1083 def get_email_alert_content(self
, results
):
1085 for evenement
in results
:
1086 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
1088 evenement
.get_absolute_url())
1089 content
+= u
' où ? : %s \n' % evenement
.lieu
1090 content
+= evenement
.debut
.strftime(u
' quand ? : %d/%m/%Y %H:%M \n')
1091 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
1092 content
+= u
' quoi ? : '
1093 content
+= '\n '.join(textwrap
.wrap(evenement
.description
))