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
)
253 class ActualiteVoir(Actualite
):
257 verbose_name
= 'actualité (visualisation)'
258 verbose_name_plural
= 'actualités (visualisation)'
262 class EvenementQuerySet(SEPQuerySet
):
264 def filter_type(self
, type):
265 return self
.filter(type=type)
267 def filter_debut(self
, min=None, max=None):
268 return self
._filter_date('debut', min=min, max=max)
270 def filter_date_modification(self
, min=None, max=None):
271 return self
._filter_date('date_modification', min=min, max=max)
273 class EvenementSphinxQuerySet(SEPSphinxQuerySet
):
275 def __init__(self
, model
=None):
276 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_evenements',
277 weights
=dict(titre
=3))
279 def filter_type(self
, type):
280 return self
.add_to_query('@type "%s"' % type)
282 def filter_debut(self
, min=None, max=None):
283 return self
._filter_date('debut', min=min, max=max)
285 def filter_date_modification(self
, min=None, max=None):
286 return self
._filter_date('date_modification', min=min, max=max)
288 class EvenementManager(SEPManager
):
290 def get_query_set(self
):
291 return EvenementQuerySet(self
.model
).filter(approuve
=True)
293 def get_sphinx_query_set(self
):
294 return EvenementSphinxQuerySet(self
.model
).order_by('-debut')
296 def filter_type(self
, type):
297 return self
.get_query_set().filter_type(type)
299 def filter_debut(self
, min=None, max=None):
300 return self
.get_query_set().filter_debut(min=min, max=max)
302 def filter_date_modification(self
, min=None, max=None):
303 return self
.get_query_set().filter_date_modification(min=min, max=max)
305 def build_time_zone_choices(pays
=None):
306 timezones
= pytz
.country_timezones
[pays
] if pays
else pytz
.common_timezones
308 now
= datetime
.datetime
.now()
309 for tzname
in timezones
:
310 tz
= pytz
.timezone(tzname
)
311 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
313 offset
= tz
.utcoffset(now
)
314 except (AmbiguousTimeError
, NonExistentTimeError
):
315 # oups. On est en train de changer d'heure. Ça devrait être fini
317 offset
= tz
.utcoffset(now
+ datetime
.timedelta(days
=1))
318 seconds
= offset
.seconds
+ offset
.days
* 86400
319 (hours
, minutes
) = divmod(seconds
// 60, 60)
320 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) if minutes
else 'UTC%+d' % hours
321 result
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
323 return [(x
[1], x
[2]) for x
in result
]
325 class Evenement(models
.Model
):
326 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
327 (u
'Conférence', u
'Conférence'),
328 (u
'Appel à contribution', u
'Appel à contribution'),
329 (u
'Journée d\'étude', u
'Journée d\'étude'),
330 (u
'Autre', u
'Autre'))
331 TIME_ZONE_CHOICES
= build_time_zone_choices()
333 uid
= models
.CharField(max_length
=255, default
=str(uuid
.uuid1()))
334 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
335 titre
= models
.CharField(max_length
=255)
336 discipline
= models
.ForeignKey('Discipline', related_name
= "discipline",
337 blank
= True, null
= True)
338 discipline_secondaire
= models
.ForeignKey('Discipline', related_name
="discipline_secondaire",
339 verbose_name
=u
"discipline secondaire",
340 blank
=True, null
=True)
341 mots_cles
= models
.TextField('Mots-Clés', blank
=True, null
=True)
342 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
343 adresse
= models
.TextField()
344 ville
= models
.CharField(max_length
=100)
345 pays
= models
.ForeignKey(Pays
, null
=True, related_name
='evenements')
346 debut
= models
.DateTimeField(default
=datetime
.datetime
.now
)
347 fin
= models
.DateTimeField(default
=datetime
.datetime
.now
)
348 fuseau
= models
.CharField(max_length
=100, choices
=TIME_ZONE_CHOICES
, verbose_name
='fuseau horaire')
349 description
= models
.TextField()
350 contact
= models
.TextField(null
=True) # champ obsolète
351 prenom
= models
.CharField('prénom', max_length
=100)
352 nom
= models
.CharField(max_length
=100)
353 courriel
= models
.EmailField()
354 url
= models
.CharField(max_length
=255, blank
=True, null
=True)
355 piece_jointe
= models
.FileField(upload_to
='agenda/pj', blank
=True, verbose_name
='pièce jointe')
356 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="evenements", verbose_name
='régions additionnelles',
357 help_text
="On considère d'emblée que l'évènement se déroule dans la région "
358 "dans laquelle se trouve le pays indiqué plus haut. Il est possible "
359 "de désigner ici des régions additionnelles.")
360 date_modification
= models
.DateTimeField(editable
=False, auto_now
=True, null
=True)
362 objects
= EvenementManager()
363 all_objects
= models
.Manager()
366 ordering
= ['-debut']
367 verbose_name
= u
'évènement'
368 verbose_name_plural
= u
'évènements'
370 def __unicode__(self
):
371 return "[%s] %s" % (self
.uid
, self
.titre
)
373 def get_absolute_url(self
):
374 return reverse('evenement', kwargs
={'id': self
.id})
376 def duration_display(self
):
377 delta
= self
.fin
- self
.debut
378 minutes
, seconds
= divmod(delta
.seconds
, 60)
379 hours
, minutes
= divmod(minutes
, 60)
383 parts
.append('1 jour')
385 parts
.append('%d jours' % days
)
387 parts
.append('1 heure')
389 parts
.append('%d heures' % hours
)
391 parts
.append('1 minute')
393 parts
.append('%d minutes' % minutes
)
394 return ' '.join(parts
)
396 def piece_jointe_display(self
):
397 return self
.piece_jointe
and os
.path
.basename(self
.piece_jointe
.name
)
399 def courriel_display(self
):
400 return self
.courriel
.replace(u
'@', u
' (à) ')
406 bits
.append(self
.adresse
)
408 bits
.append(self
.ville
)
410 bits
.append(self
.pays
.nom
)
411 return ', '.join(bits
)
414 from django
.core
.exceptions
import ValidationError
415 if self
.debut
> self
.fin
:
416 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
418 def save(self
, *args
, **kwargs
):
419 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
421 self
.contact
= '' # Vider ce champ obsolète à la première occasion...
423 super(Evenement
, self
).save(*args
, **kwargs
)
426 # methodes de commnunications avec CALDAV
428 """Retourne l'evenement django sous forme d'objet icalendar"""
429 cal
= vobject
.iCalendar()
432 # fournit son propre uid
433 if self
.uid
in [None, ""]:
434 self
.uid
= str(uuid
.uuid1())
436 cal
.vevent
.add('uid').value
= self
.uid
438 cal
.vevent
.add('summary').value
= self
.titre
440 if self
.mots_cles
is None:
443 kw
= self
.mots_cles
.split(",")
446 kw
.append(self
.discipline
.nom
)
447 kw
.append(self
.discipline_secondaire
.nom
)
451 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
453 cal
.vevent
.add('x-auf-keywords').value
= k
455 description
= self
.description
457 if len(self
.description
) > 0:
459 description
+= u
"Mots-clés: " + ", ".join(kw
)
461 cal
.vevent
.add('dtstart').value
= combine(self
.debut
, pytz
.timezone(self
.fuseau
))
462 cal
.vevent
.add('dtend').value
= combine(self
.fin
, pytz
.timezone(self
.fuseau
))
463 cal
.vevent
.add('created').value
= combine(datetime
.datetime
.now(), "UTC")
464 cal
.vevent
.add('dtstamp').value
= combine(datetime
.datetime
.now(), "UTC")
465 if len(description
) > 0:
466 cal
.vevent
.add('description').value
= description
467 if len(self
.contact
) > 0:
468 cal
.vevent
.add('contact').value
= self
.contact
469 if len(self
.url
) > 0:
470 cal
.vevent
.add('url').value
= self
.url
471 cal
.vevent
.add('location').value
= ', '.join([x
for x
in [self
.adresse
, self
.ville
, self
.pays
.nom
] if x
])
472 if self
.piece_jointe
:
473 url
= self
.piece_jointe
.url
474 if not url
.startswith('http://'):
475 url
= SITE_ROOT_URL
+ url
476 cal
.vevent
.add('attach').value
= url
479 def update_vevent(self
,):
480 """Essaie de créer l'évènement sur le serveur ical.
481 En cas de succès, l'évènement local devient donc inactif et approuvé"""
484 event
= self
.as_ical()
485 client
= caldav
.DAVClient(CALENDRIER_URL
)
486 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
487 e
= caldav
.Event(client
, parent
= cal
, data
= event
.serialize(), id=self
.uid
)
490 self
.approuve
= False
492 def delete_vevent(self
,):
493 """Supprime l'evenement sur le serveur caldav"""
496 event
= self
.as_ical()
497 client
= caldav
.DAVClient(CALENDRIER_URL
)
498 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
499 e
= cal
.event(self
.uid
)
501 except error
.NotFoundError
:
504 def assigner_regions(self
, regions
):
505 self
.regions
.add(*regions
)
507 def assigner_disciplines(self
, disciplines
):
508 if len(disciplines
) == 1:
510 self
.discipline_secondaire
= disciplines
[0]
512 self
.discipline
= disciplines
[0]
513 elif len(disciplines
) >= 2:
514 self
.discipline
= disciplines
[0]
515 self
.discipline_secondaire
= disciplines
[1]
517 def delete_vevent(sender
, instance
, *args
, **kwargs
):
518 # Surcharge du comportement de suppression
519 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
520 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
521 instance
.delete_vevent()
522 pre_delete
.connect(delete_vevent
, sender
=Evenement
)
524 class EvenementVoir(Evenement
):
528 verbose_name
= 'événement (visualisation)'
529 verbose_name_plural
= 'événement (visualisation)'
533 class ListSet(models
.Model
):
534 spec
= models
.CharField(primary_key
= True, max_length
= 255)
535 name
= models
.CharField(max_length
= 255)
536 server
= models
.CharField(max_length
= 255)
537 validated
= models
.BooleanField(default
= True)
539 def __unicode__(self
,):
542 class RecordCategorie(models
.Model
):
543 nom
= models
.CharField(max_length
=255)
546 verbose_name
= 'catégorie ressource'
547 verbose_name_plural
= 'catégories ressources'
549 def __unicode__(self
):
553 class RecordQuerySet(SEPQuerySet
):
555 def filter_modified(self
, min=None, max=None):
556 return self
._filter_date('modified', min=min, max=max)
558 class RecordSphinxQuerySet(SEPSphinxQuerySet
):
560 def __init__(self
, model
=None):
561 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_ressources',
562 weights
=dict(title
=3))
564 def filter_modified(self
, min=None, max=None):
565 return self
._filter_date('modified', min=min, max=max)
567 class RecordManager(SEPManager
):
569 def get_query_set(self
):
570 """Ne garder que les ressources validées et qui sont soit dans aucun
571 listset ou au moins dans un listset validé."""
572 qs
= RecordQuerySet(self
.model
)
573 qs
= qs
.filter(validated
=True)
574 qs
= qs
.filter(Q(listsets__isnull
=True) |
Q(listsets__validated
=True))
577 def get_sphinx_query_set(self
):
578 return RecordSphinxQuerySet(self
.model
)
580 def filter_modified(self
, min=None, max=None):
581 return self
.get_query_set().filter_modified(min=min, max=max)
583 class Record(models
.Model
):
585 #fonctionnement interne
586 id = models
.AutoField(primary_key
= True)
587 server
= models
.CharField(max_length
= 255, verbose_name
=u
'serveur')
588 last_update
= models
.CharField(max_length
= 255)
589 last_checksum
= models
.CharField(max_length
= 255)
590 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
593 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
594 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
595 description
= models
.TextField(null
=True, blank
=True)
596 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
597 identifier
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
598 uri
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
599 source
= models
.TextField(null
= True, blank
= True)
600 contributor
= models
.TextField(null
= True, blank
= True)
601 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
602 publisher
= models
.TextField(null
= True, blank
= True)
603 type = models
.TextField(null
= True, blank
= True)
604 format
= models
.TextField(null
= True, blank
= True)
605 language
= models
.TextField(null
= True, blank
= True)
607 listsets
= models
.ManyToManyField(ListSet
, null
= True, blank
= True)
609 #SEP 2 (aucune données récoltées)
610 alt_title
= models
.TextField(null
= True, blank
= True)
611 abstract
= models
.TextField(null
= True, blank
= True)
612 creation
= models
.CharField(max_length
= 255, null
= True, blank
= True)
613 issued
= models
.CharField(max_length
= 255, null
= True, blank
= True)
614 isbn
= models
.TextField(null
= True, blank
= True)
615 orig_lang
= models
.TextField(null
= True, blank
= True)
617 categorie
= models
.ForeignKey(RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie')
619 # Metadata AUF multivaluées
620 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
621 thematiques
= models
.ManyToManyField(Thematique
, blank
=True, verbose_name
='thématiques')
622 pays
= models
.ManyToManyField(Pays
, blank
=True)
623 regions
= models
.ManyToManyField(Region
, blank
=True, verbose_name
='régions')
626 objects
= RecordManager()
627 all_objects
= models
.Manager()
630 verbose_name
= 'ressource'
632 def __unicode__(self
):
633 return "[%s] %s" % (self
.server
, self
.title
)
635 def get_absolute_url(self
):
636 return reverse('ressource', kwargs
={'id': self
.id})
638 def getServeurURL(self
):
639 """Retourne l'URL du serveur de provenance"""
640 return RESOURCES
[self
.server
]['url']
642 def est_complet(self
):
643 """teste si le record à toutes les données obligatoires"""
644 return self
.disciplines
.count() > 0 and \
645 self
.thematiques
.count() > 0 and \
646 self
.pays
.count() > 0 and \
647 self
.regions
.count() > 0
649 def assigner_regions(self
, regions
):
650 self
.regions
.add(*regions
)
652 def assigner_disciplines(self
, disciplines
):
653 self
.disciplines
.add(*disciplines
)
655 class RecordEdit(Record
):
659 verbose_name
= 'ressource (édition)'
660 verbose_name_plural
= 'ressources (édition)'
662 class Serveur(models
.Model
):
663 """Identification d'un serveur d'ou proviennent les références"""
664 nom
= models
.CharField(primary_key
= True, max_length
= 255)
666 def __unicode__(self
,):
669 def conf_2_db(self
,):
670 for k
in RESOURCES
.keys():
671 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
675 class Profile(models
.Model
):
676 user
= models
.ForeignKey(User
, unique
=True)
677 serveurs
= models
.ManyToManyField(Serveur
, null
= True, blank
= True)
679 class HarvestLog(models
.Model
):
680 context
= models
.CharField(max_length
= 255)
681 name
= models
.CharField(max_length
= 255)
682 date
= models
.DateTimeField(auto_now
= True)
683 added
= models
.IntegerField(null
= True, blank
= True)
684 updated
= models
.IntegerField(null
= True, blank
= True)
685 processed
= models
.IntegerField(null
= True, blank
= True)
686 record
= models
.ForeignKey(Record
, null
= True, blank
= True)
690 logger
= HarvestLog()
691 if message
.has_key('record_id'):
692 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
693 del(message
['record_id'])
695 for k
,v
in message
.items():
696 setattr(logger
, k
, v
)
701 class PageStatique(models
.Model
):
702 id = models
.CharField(max_length
=32, primary_key
=True)
703 titre
= models
.CharField(max_length
=100)
704 contenu
= models
.TextField()
707 verbose_name_plural
= 'pages statiques'
711 class GlobalSearchResults(object):
713 def __init__(self
, actualites
=None, appels
=None, evenements
=None,
714 ressources
=None, chercheurs
=None, groupes
=None,
715 sites
=None, sites_auf
=None):
716 self
.actualites
= actualites
718 self
.evenements
= evenements
719 self
.ressources
= ressources
720 self
.chercheurs
= chercheurs
721 self
.groupes
= groupes
723 self
.sites_auf
= sites_auf
725 def __nonzero__(self
):
726 return bool(self
.actualites
or self
.appels
or self
.evenements
or
727 self
.ressources
or self
.chercheurs
or self
.groupes
or
728 self
.sites
or self
.sites_auf
)
730 class Search(models
.Model
):
731 user
= models
.ForeignKey(User
, editable
=False)
732 content_type
= models
.ForeignKey(ContentType
, editable
=False)
733 nom
= models
.CharField(max_length
=100, verbose_name
="nom de la recherche")
734 alerte_courriel
= models
.BooleanField(verbose_name
="Envoyer une alerte courriel")
735 derniere_alerte
= models
.DateField(verbose_name
="Date d'envoi de la dernière alerte courriel", null
=True, editable
=False)
736 q
= models
.CharField(max_length
=100, blank
=True, verbose_name
="dans tous les champs")
737 discipline
= models
.ForeignKey(Discipline
, blank
=True, null
=True)
738 region
= models
.ForeignKey(Region
, blank
=True, null
=True, verbose_name
='région',
739 help_text
="La région est ici définie au sens, non strictement géographique, du Bureau régional de l'AUF de référence.")
741 def query_string(self
):
743 for field
in self
._meta
.fields
:
744 if field
.name
in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
746 value
= getattr(self
, field
.column
)
748 if isinstance(value
, datetime
.date
):
749 params
[field
.name
] = value
.strftime('%d/%m/%Y')
751 params
[field
.name
] = smart_str(value
)
752 return urlencode(params
)
755 verbose_name
= 'recherche transversale'
756 verbose_name_plural
= "recherches transversales"
758 def __unicode__(self
):
762 if self
.alerte_courriel
:
764 original_search
= Search
.objects
.get(id=self
.id)
765 if not original_search
.alerte_courriel
:
766 # On a nouvellement activé l'alerte courriel. Notons la
768 self
.derniere_alerte
= datetime
.date
.today() - datetime
.timedelta(days
=1)
769 except Search
.DoesNotExist
:
770 self
.derniere_alerte
= datetime
.date
.today() - datetime
.timedelta(days
=1)
771 if (not self
.content_type_id
):
772 self
.content_type
= ContentType
.objects
.get_for_model(self
.__class__
)
773 super(Search
, self
).save()
775 def as_leaf_class(self
):
776 content_type
= self
.content_type
777 model
= content_type
.model_class()
780 return model
.objects
.get(id=self
.id)
782 def run(self
, min_date
=None, max_date
=None):
783 from chercheurs
.models
import Chercheur
, Groupe
784 from sitotheque
.models
import Site
786 actualites
= Actualite
.objects
787 evenements
= Evenement
.objects
788 ressources
= Record
.objects
789 chercheurs
= Chercheur
.objects
790 groupes
= Groupe
.objects
793 actualites
= actualites
.search(self
.q
)
794 evenements
= evenements
.search(self
.q
)
795 ressources
= ressources
.search(self
.q
)
796 chercheurs
= chercheurs
.search(self
.q
)
797 groupes
= groupes
.search(self
.q
)
798 sites
= sites
.search(self
.q
)
800 actualites
= actualites
.filter_discipline(self
.discipline
)
801 evenements
= evenements
.filter_discipline(self
.discipline
)
802 ressources
= ressources
.filter_discipline(self
.discipline
)
803 chercheurs
= chercheurs
.filter_discipline(self
.discipline
)
804 sites
= sites
.filter_discipline(self
.discipline
)
806 actualites
= actualites
.filter_region(self
.region
)
807 evenements
= evenements
.filter_region(self
.region
)
808 ressources
= ressources
.filter_region(self
.region
)
809 chercheurs
= chercheurs
.filter_region(self
.region
)
810 sites
= sites
.filter_region(self
.region
)
812 actualites
= actualites
.filter_date(min=min_date
)
813 evenements
= evenements
.filter_date_modification(min=min_date
)
814 ressources
= ressources
.filter_modified(min=min_date
)
815 chercheurs
= chercheurs
.filter_date_modification(min=min_date
)
816 sites
= sites
.filter_date_maj(min=min_date
)
818 actualites
= actualites
.filter_date(max=max_date
)
819 evenements
= evenements
.filter_date_modification(max=max_date
)
820 ressources
= ressources
.filter_modified(max=max_date
)
821 chercheurs
= chercheurs
.filter_date_modification(max=max_date
)
822 sites
= sites
.filter_date_maj(max=max_date
)
825 sites_auf
= google_search(0, self
.q
)['results']
829 return GlobalSearchResults(
830 actualites
=actualites
.order_by('-date').filter_type('actu'),
831 appels
=actualites
.order_by('-date').filter_type('appels'),
832 evenements
=evenements
.order_by('-debut'),
833 ressources
=ressources
.order_by('-id'),
834 chercheurs
=chercheurs
.order_by('-date_modification'),
835 groupes
=groupes
.order_by('nom'),
836 sites
=sites
.order_by('-date_maj'),
842 if self
.content_type
.model
!= 'search':
843 obj
= self
.content_type
.get_object_for_this_type(pk
=self
.pk
)
848 url
+= '/discipline/%d' % self
.discipline
.id
850 url
+= '/region/%d' % self
.region
.id
853 url
+= '?' + urlencode({'q': smart_str(self
.q
)})
859 def send_email_alert(self
):
860 """Envoie une alerte courriel correspondant à cette recherche"""
861 yesterday
= datetime
.date
.today() - datetime
.timedelta(days
=1)
862 if self
.derniere_alerte
is not None:
863 results
= self
.as_leaf_class().run(min_date
=self
.derniere_alerte
, max_date
=yesterday
)
865 subject
= 'Savoirs en partage - ' + self
.nom
866 from_email
= CONTACT_EMAIL
867 to_email
= self
.user
.email
868 text_content
= u
'Voici les derniers résultats correspondant à votre recherche sauvegardée.\n\n'
869 text_content
+= self
.as_leaf_class().get_email_alert_content(results
)
872 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
873 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (SITE_ROOT_URL
, reverse('recherches'))
874 html_content
= '<div style="font-family: Arial, sans-serif">\n' + markdown(smart_str(text_content
)) + '</div>\n'
875 msg
= EmailMultiAlternatives(subject
, text_content
, from_email
, [to_email
])
876 msg
.attach_alternative(html_content
, "text/html")
878 self
.derniere_alerte
= yesterday
881 def get_email_alert_content(self
, results
):
883 if results
.chercheurs
:
884 content
+= u
'\n### Nouveaux chercheurs\n\n'
885 for chercheur
in results
.chercheurs
:
886 content
+= u
'- [%s %s](%s%s) \n' % (chercheur
.nom
.upper(),
889 chercheur
.get_absolute_url())
890 content
+= u
' %s\n\n' % chercheur
.etablissement_display
891 if results
.ressources
:
892 content
+= u
'\n### Nouvelles ressources\n\n'
893 for ressource
in results
.ressources
:
894 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
896 ressource
.get_absolute_url())
897 if ressource
.description
:
899 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(ressource
.description
)])
902 if results
.actualites
:
903 content
+= u
'\n### Nouvelles actualités\n\n'
904 for actualite
in results
.actualites
:
905 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
907 actualite
.get_absolute_url())
910 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(actualite
.texte
)])
913 content
+= u
"\n### Nouveaux appels d'offres\n\n"
914 for appel
in results
.appels
:
915 content
+= u
'- [%s](%s%s)\n\n' % (appel
.titre
,
917 appel
.get_absolute_url())
920 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(appel
.texte
)])
922 if results
.evenements
:
923 content
+= u
"\n### Nouveaux évènements\n\n"
924 for evenement
in results
.evenements
:
925 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
927 evenement
.get_absolute_url())
928 content
+= u
' où ? : %s \n' % evenement
.lieu
929 content
+= evenement
.debut
.strftime(u
' quand ? : %d/%m/%Y %H:%M \n')
930 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
931 content
+= u
' quoi ? : '
932 content
+= '\n '.join(textwrap
.wrap(evenement
.description
))
935 content
+= u
"\n### Nouveaux sites\n\n"
936 for site
in results
.sites
:
937 content
+= u
'- [%s](%s%s)\n\n' % (site
.titre
,
939 site
.get_absolute_url())
942 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(site
.description
)])
946 class RessourceSearch(Search
):
947 auteur
= models
.CharField(max_length
=100, blank
=True, verbose_name
="auteur ou contributeur")
948 titre
= models
.CharField(max_length
=100, blank
=True)
949 sujet
= models
.CharField(max_length
=100, blank
=True)
950 publisher
= models
.CharField(max_length
=100, blank
=True, verbose_name
="éditeur")
951 categorie
= models
.ForeignKey(RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie')
954 verbose_name
= 'recherche de ressources'
955 verbose_name_plural
= "recherches de ressources"
957 def run(self
, min_date
=None, max_date
=None):
958 results
= Record
.objects
960 results
= results
.search(self
.q
)
962 results
= results
.add_to_query('@(creator,contributor) ' + self
.auteur
)
964 results
= results
.add_to_query('@title ' + self
.titre
)
966 results
= results
.add_to_query('@subject ' + self
.sujet
)
968 results
= results
.add_to_query('@publisher ' + self
.publisher
)
970 results
= results
.add_to_query('@categorie %s' % self
.categorie
.id)
972 results
= results
.filter_discipline(self
.discipline
)
974 results
= results
.filter_region(self
.region
)
976 results
= results
.filter_modified(min=min_date
)
978 results
= results
.filter_modified(max=max_date
)
980 """Montrer les résultats les plus récents si on n'a pas fait
981 une recherche par mots-clés."""
982 results
= results
.order_by('-modified')
986 qs
= self
.query_string()
987 return reverse('ressources') + ('?' + qs
if qs
else '')
990 qs
= self
.query_string()
991 return reverse('rss_ressources') + ('?' + qs
if qs
else '')
993 def get_email_alert_content(self
, results
):
995 for ressource
in results
:
996 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
998 ressource
.get_absolute_url())
999 if ressource
.description
:
1001 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(ressource
.description
)])
1005 class ActualiteSearchBase(Search
):
1006 date_min
= models
.DateField(blank
=True, null
=True, verbose_name
="depuis le")
1007 date_max
= models
.DateField(blank
=True, null
=True, verbose_name
="jusqu'au")
1012 def run(self
, min_date
=None, max_date
=None):
1013 results
= Actualite
.objects
1015 results
= results
.search(self
.q
)
1017 results
= results
.filter_discipline(self
.discipline
)
1019 results
= results
.filter_region(self
.region
)
1021 results
= results
.filter_date(min=self
.date_min
)
1023 results
= results
.filter_date(max=self
.date_max
)
1025 results
= results
.filter_date(min=min_date
)
1027 results
= results
.filter_date(max=max_date
)
1028 return results
.all()
1030 def get_email_alert_content(self
, results
):
1032 for actualite
in results
:
1033 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
1035 actualite
.get_absolute_url())
1038 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(actualite
.texte
)])
1042 class ActualiteSearch(ActualiteSearchBase
):
1045 verbose_name
= "recherche d'actualités"
1046 verbose_name_plural
= "recherches d'actualités"
1048 def run(self
, min_date
=None, max_date
=None):
1049 return super(ActualiteSearch
, self
).run(min_date
=min_date
, max_date
=max_date
).filter_type('actu')
1052 qs
= self
.query_string()
1053 return reverse('actualites') + ('?' + qs
if qs
else '')
1056 qs
= self
.query_string()
1057 return reverse('rss_actualites') + ('?' + qs
if qs
else '')
1059 class AppelSearch(ActualiteSearchBase
):
1062 verbose_name
= "recherche d'appels d'offres"
1063 verbose_name_plural
= "recherches d'appels d'offres"
1065 def run(self
, min_date
=None, max_date
=None):
1066 return super(AppelSearch
, self
).run(min_date
=min_date
, max_date
=max_date
).filter_type('appels')
1069 qs
= self
.query_string()
1070 return reverse('appels') + ('?' + qs
if qs
else '')
1073 qs
= self
.query_string()
1074 return reverse('rss_appels') + ('?' + qs
if qs
else '')
1076 class EvenementSearch(Search
):
1077 titre
= models
.CharField(max_length
=100, blank
=True, verbose_name
="Intitulé")
1078 type = models
.CharField(max_length
=100, blank
=True, choices
=Evenement
.TYPE_CHOICES
)
1079 date_min
= models
.DateField(blank
=True, null
=True, verbose_name
="depuis le")
1080 date_max
= models
.DateField(blank
=True, null
=True, verbose_name
="jusqu'au")
1083 verbose_name
= "recherche d'évènements"
1084 verbose_name_plural
= "recherches d'évènements"
1086 def run(self
, min_date
=None, max_date
=None):
1087 results
= Evenement
.objects
1089 results
= results
.search(self
.q
)
1091 results
= results
.add_to_query('@titre ' + self
.titre
)
1093 results
= results
.filter_discipline(self
.discipline
)
1095 results
= results
.filter_region(self
.region
)
1097 results
= results
.filter_type(self
.type)
1099 results
= results
.filter_debut(min=self
.date_min
)
1101 results
= results
.filter_debut(max=self
.date_max
)
1103 results
= results
.filter_date_modification(min=min_date
)
1105 results
= results
.filter_date_modification(max=max_date
)
1106 return results
.all()
1109 qs
= self
.query_string()
1110 return reverse('agenda') + ('?' + qs
if qs
else '')
1113 qs
= self
.query_string()
1114 return reverse('rss_agenda') + ('?' + qs
if qs
else '')
1116 def get_email_alert_content(self
, results
):
1118 for evenement
in results
:
1119 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
1121 evenement
.get_absolute_url())
1122 content
+= u
' où ? : %s \n' % evenement
.lieu
1123 content
+= evenement
.debut
.strftime(u
' quand ? : %d/%m/%Y %H:%M \n')
1124 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
1125 content
+= u
' quoi ? : '
1126 content
+= '\n '.join(textwrap
.wrap(evenement
.description
))