ae2bbc38a4ea126d4ce348b23e603583532e8944
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 Serveur(models
.Model
):
656 """Identification d'un serveur d'ou proviennent les références"""
657 nom
= models
.CharField(primary_key
= True, max_length
= 255)
659 def __unicode__(self
,):
662 def conf_2_db(self
,):
663 for k
in RESOURCES
.keys():
664 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
668 class Profile(models
.Model
):
669 user
= models
.ForeignKey(User
, unique
=True)
670 serveurs
= models
.ManyToManyField(Serveur
, null
= True, blank
= True)
672 class HarvestLog(models
.Model
):
673 context
= models
.CharField(max_length
= 255)
674 name
= models
.CharField(max_length
= 255)
675 date
= models
.DateTimeField(auto_now
= True)
676 added
= models
.IntegerField(null
= True, blank
= True)
677 updated
= models
.IntegerField(null
= True, blank
= True)
678 processed
= models
.IntegerField(null
= True, blank
= True)
679 record
= models
.ForeignKey(Record
, null
= True, blank
= True)
683 logger
= HarvestLog()
684 if message
.has_key('record_id'):
685 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
686 del(message
['record_id'])
688 for k
,v
in message
.items():
689 setattr(logger
, k
, v
)
694 class PageStatique(models
.Model
):
695 id = models
.CharField(max_length
=32, primary_key
=True)
696 titre
= models
.CharField(max_length
=100)
697 contenu
= models
.TextField()
700 verbose_name_plural
= 'pages statiques'
704 class GlobalSearchResults(object):
706 def __init__(self
, actualites
=None, appels
=None, evenements
=None,
707 ressources
=None, chercheurs
=None, groupes
=None,
708 sites
=None, sites_auf
=None):
709 self
.actualites
= actualites
711 self
.evenements
= evenements
712 self
.ressources
= ressources
713 self
.chercheurs
= chercheurs
714 self
.groupes
= groupes
716 self
.sites_auf
= sites_auf
718 def __nonzero__(self
):
719 return bool(self
.actualites
or self
.appels
or self
.evenements
or
720 self
.ressources
or self
.chercheurs
or self
.groupes
or
721 self
.sites
or self
.sites_auf
)
723 class Search(models
.Model
):
724 user
= models
.ForeignKey(User
, editable
=False)
725 content_type
= models
.ForeignKey(ContentType
, editable
=False)
726 nom
= models
.CharField(max_length
=100, verbose_name
="nom de la recherche")
727 alerte_courriel
= models
.BooleanField(verbose_name
="Envoyer une alerte courriel")
728 derniere_alerte
= models
.DateField(verbose_name
="Date d'envoi de la dernière alerte courriel", null
=True, editable
=False)
729 q
= models
.CharField(max_length
=100, blank
=True, verbose_name
="rechercher dans tous les champs")
730 discipline
= models
.ForeignKey(Discipline
, blank
=True, null
=True)
731 region
= models
.ForeignKey(Region
, blank
=True, null
=True, verbose_name
='région',
732 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.")
734 def query_string(self
):
736 for field
in self
._meta
.fields
:
737 if field
.name
in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
739 value
= getattr(self
, field
.column
)
741 if isinstance(value
, datetime
.date
):
742 params
[field
.name
] = value
.strftime('%d/%m/%Y')
744 params
[field
.name
] = smart_str(value
)
745 return urlencode(params
)
748 verbose_name
= 'recherche transversale'
749 verbose_name_plural
= "recherches transversales"
751 def __unicode__(self
):
755 if self
.alerte_courriel
:
757 original_search
= Search
.objects
.get(id=self
.id)
758 if not original_search
.alerte_courriel
:
759 # On a nouvellement activé l'alerte courriel. Notons la
761 self
.derniere_alerte
= datetime
.date
.today() - datetime
.timedelta(days
=1)
762 except Search
.DoesNotExist
:
763 self
.derniere_alerte
= datetime
.date
.today() - datetime
.timedelta(days
=1)
764 if (not self
.content_type_id
):
765 self
.content_type
= ContentType
.objects
.get_for_model(self
.__class__
)
766 super(Search
, self
).save()
768 def as_leaf_class(self
):
769 content_type
= self
.content_type
770 model
= content_type
.model_class()
773 return model
.objects
.get(id=self
.id)
775 def run(self
, min_date
=None, max_date
=None):
776 from chercheurs
.models
import Chercheur
, Groupe
777 from sitotheque
.models
import Site
779 actualites
= Actualite
.objects
780 evenements
= Evenement
.objects
781 ressources
= Record
.objects
782 chercheurs
= Chercheur
.objects
783 groupes
= Groupe
.objects
786 actualites
= actualites
.search(self
.q
)
787 evenements
= evenements
.search(self
.q
)
788 ressources
= ressources
.search(self
.q
)
789 chercheurs
= chercheurs
.search(self
.q
)
790 groupes
= groupes
.search(self
.q
)
791 sites
= sites
.search(self
.q
)
793 actualites
= actualites
.filter_discipline(self
.discipline
)
794 evenements
= evenements
.filter_discipline(self
.discipline
)
795 ressources
= ressources
.filter_discipline(self
.discipline
)
796 chercheurs
= chercheurs
.filter_discipline(self
.discipline
)
797 sites
= sites
.filter_discipline(self
.discipline
)
799 actualites
= actualites
.filter_region(self
.region
)
800 evenements
= evenements
.filter_region(self
.region
)
801 ressources
= ressources
.filter_region(self
.region
)
802 chercheurs
= chercheurs
.filter_region(self
.region
)
803 sites
= sites
.filter_region(self
.region
)
805 actualites
= actualites
.filter_date(min=min_date
)
806 evenements
= evenements
.filter_date_modification(min=min_date
)
807 ressources
= ressources
.filter_modified(min=min_date
)
808 chercheurs
= chercheurs
.filter_date_modification(min=min_date
)
809 sites
= sites
.filter_date_maj(min=min_date
)
811 actualites
= actualites
.filter_date(max=max_date
)
812 evenements
= evenements
.filter_date_modification(max=max_date
)
813 ressources
= ressources
.filter_modified(max=max_date
)
814 chercheurs
= chercheurs
.filter_date_modification(max=max_date
)
815 sites
= sites
.filter_date_maj(max=max_date
)
818 sites_auf
= google_search(0, self
.q
)['results']
822 return GlobalSearchResults(
823 actualites
=actualites
.order_by('-date').filter_type('actu'),
824 appels
=actualites
.order_by('-date').filter_type('appels'),
825 evenements
=evenements
.order_by('-debut'),
826 ressources
=ressources
.order_by('-id'),
827 chercheurs
=chercheurs
.order_by('-date_modification'),
828 groupes
=groupes
.order_by('nom'),
829 sites
=sites
.order_by('-date_maj'),
835 if self
.content_type
.model
!= 'search':
836 obj
= self
.content_type
.get_object_for_this_type(pk
=self
.pk
)
841 url
+= '/discipline/%d' % self
.discipline
.id
843 url
+= '/region/%d' % self
.region
.id
846 url
+= '?' + urlencode({'q': smart_str(self
.q
)})
852 def send_email_alert(self
):
853 """Envoie une alerte courriel correspondant à cette recherche"""
854 yesterday
= datetime
.date
.today() - datetime
.timedelta(days
=1)
855 if self
.derniere_alerte
is not None:
856 results
= self
.as_leaf_class().run(min_date
=self
.derniere_alerte
, max_date
=yesterday
)
858 subject
= 'Savoirs en partage - ' + self
.nom
859 from_email
= CONTACT_EMAIL
860 to_email
= self
.user
.email
861 text_content
= u
'Voici les derniers résultats correspondant à votre recherche sauvegardée.\n\n'
862 text_content
+= self
.as_leaf_class().get_email_alert_content(results
)
865 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
866 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (SITE_ROOT_URL
, reverse('recherches'))
867 html_content
= '<div style="font-family: Arial, sans-serif">\n' + markdown(smart_str(text_content
)) + '</div>\n'
868 msg
= EmailMultiAlternatives(subject
, text_content
, from_email
, [to_email
])
869 msg
.attach_alternative(html_content
, "text/html")
871 self
.derniere_alerte
= yesterday
874 def get_email_alert_content(self
, results
):
876 if results
.chercheurs
:
877 content
+= u
'\n### Nouveaux chercheurs\n\n'
878 for chercheur
in results
.chercheurs
:
879 content
+= u
'- [%s %s](%s%s) \n' % (chercheur
.nom
.upper(),
882 chercheur
.get_absolute_url())
883 content
+= u
' %s\n\n' % chercheur
.etablissement_display
884 if results
.ressources
:
885 content
+= u
'\n### Nouvelles ressources\n\n'
886 for ressource
in results
.ressources
:
887 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
889 ressource
.get_absolute_url())
890 if ressource
.description
:
892 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(ressource
.description
)])
895 if results
.actualites
:
896 content
+= u
'\n### Nouvelles actualités\n\n'
897 for actualite
in results
.actualites
:
898 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
900 actualite
.get_absolute_url())
903 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(actualite
.texte
)])
906 content
+= u
"\n### Nouveaux appels d'offres\n\n"
907 for appel
in results
.appels
:
908 content
+= u
'- [%s](%s%s)\n\n' % (appel
.titre
,
910 appel
.get_absolute_url())
913 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(appel
.texte
)])
915 if results
.evenements
:
916 content
+= u
"\n### Nouveaux évènements\n\n"
917 for evenement
in results
.evenements
:
918 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
920 evenement
.get_absolute_url())
921 content
+= u
' où ? : %s \n' % evenement
.lieu
922 content
+= evenement
.debut
.strftime(u
' quand ? : %d/%m/%Y %H:%M \n')
923 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
924 content
+= u
' quoi ? : '
925 content
+= '\n '.join(textwrap
.wrap(evenement
.description
))
928 content
+= u
"\n### Nouveaux sites\n\n"
929 for site
in results
.sites
:
930 content
+= u
'- [%s](%s%s)\n\n' % (site
.titre
,
932 site
.get_absolute_url())
935 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(site
.description
)])
939 class RessourceSearch(Search
):
940 auteur
= models
.CharField(max_length
=100, blank
=True, verbose_name
="auteur ou contributeur")
941 titre
= models
.CharField(max_length
=100, blank
=True)
942 sujet
= models
.CharField(max_length
=100, blank
=True)
943 publisher
= models
.CharField(max_length
=100, blank
=True, verbose_name
="éditeur")
944 categorie
= models
.ForeignKey(RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie')
947 verbose_name
= 'recherche de ressources'
948 verbose_name_plural
= "recherches de ressources"
950 def run(self
, min_date
=None, max_date
=None):
951 results
= Record
.objects
953 results
= results
.search(self
.q
)
955 results
= results
.add_to_query('@(creator,contributor) ' + self
.auteur
)
957 results
= results
.add_to_query('@title ' + self
.titre
)
959 results
= results
.add_to_query('@subject ' + self
.sujet
)
961 results
= results
.add_to_query('@publisher ' + self
.publisher
)
963 results
= results
.add_to_query('@categorie %s' % self
.categorie
.id)
965 results
= results
.filter_discipline(self
.discipline
)
967 results
= results
.filter_region(self
.region
)
969 results
= results
.filter_modified(min=min_date
)
971 results
= results
.filter_modified(max=max_date
)
973 """Montrer les résultats les plus récents si on n'a pas fait
974 une recherche par mots-clés."""
975 results
= results
.order_by('-modified')
979 qs
= self
.query_string()
980 return reverse('ressources') + ('?' + qs
if qs
else '')
983 qs
= self
.query_string()
984 return reverse('rss_ressources') + ('?' + qs
if qs
else '')
986 def get_email_alert_content(self
, results
):
988 for ressource
in results
:
989 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
991 ressource
.get_absolute_url())
992 if ressource
.description
:
994 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(ressource
.description
)])
998 class ActualiteSearchBase(Search
):
999 date_min
= models
.DateField(blank
=True, null
=True, verbose_name
="depuis le")
1000 date_max
= models
.DateField(blank
=True, null
=True, verbose_name
="jusqu'au")
1005 def run(self
, min_date
=None, max_date
=None):
1006 results
= Actualite
.objects
1008 results
= results
.search(self
.q
)
1010 results
= results
.filter_discipline(self
.discipline
)
1012 results
= results
.filter_region(self
.region
)
1014 results
= results
.filter_date(min=self
.date_min
)
1016 results
= results
.filter_date(max=self
.date_max
)
1018 results
= results
.filter_date(min=min_date
)
1020 results
= results
.filter_date(max=max_date
)
1021 return results
.all()
1023 def get_email_alert_content(self
, results
):
1025 for actualite
in results
:
1026 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
1028 actualite
.get_absolute_url())
1031 content
+= ''.join([' %s\n' % line
for line
in textwrap
.wrap(actualite
.texte
)])
1035 class ActualiteSearch(ActualiteSearchBase
):
1038 verbose_name
= "recherche d'actualités"
1039 verbose_name_plural
= "recherches d'actualités"
1041 def run(self
, min_date
=None, max_date
=None):
1042 return super(ActualiteSearch
, self
).run(min_date
=min_date
, max_date
=max_date
).filter_type('actu')
1045 qs
= self
.query_string()
1046 return reverse('actualites') + ('?' + qs
if qs
else '')
1049 qs
= self
.query_string()
1050 return reverse('rss_actualites') + ('?' + qs
if qs
else '')
1052 class AppelSearch(ActualiteSearchBase
):
1055 verbose_name
= "recherche d'appels d'offres"
1056 verbose_name_plural
= "recherches d'appels d'offres"
1058 def run(self
, min_date
=None, max_date
=None):
1059 return super(AppelSearch
, self
).run(min_date
=min_date
, max_date
=max_date
).filter_type('appels')
1062 qs
= self
.query_string()
1063 return reverse('appels') + ('?' + qs
if qs
else '')
1066 qs
= self
.query_string()
1067 return reverse('rss_appels') + ('?' + qs
if qs
else '')
1069 class EvenementSearch(Search
):
1070 titre
= models
.CharField(max_length
=100, blank
=True, verbose_name
="Intitulé")
1071 type = models
.CharField(max_length
=100, blank
=True, choices
=Evenement
.TYPE_CHOICES
)
1072 date_min
= models
.DateField(blank
=True, null
=True, verbose_name
="depuis le")
1073 date_max
= models
.DateField(blank
=True, null
=True, verbose_name
="jusqu'au")
1076 verbose_name
= "recherche d'évènements"
1077 verbose_name_plural
= "recherches d'évènements"
1079 def run(self
, min_date
=None, max_date
=None):
1080 results
= Evenement
.objects
1082 results
= results
.search(self
.q
)
1084 results
= results
.add_to_query('@titre ' + self
.titre
)
1086 results
= results
.filter_discipline(self
.discipline
)
1088 results
= results
.filter_region(self
.region
)
1090 results
= results
.filter_type(self
.type)
1092 results
= results
.filter_debut(min=self
.date_min
)
1094 results
= results
.filter_debut(max=self
.date_max
)
1096 results
= results
.filter_date_modification(min=min_date
)
1098 results
= results
.filter_date_modification(max=max_date
)
1099 return results
.all()
1102 qs
= self
.query_string()
1103 return reverse('agenda') + ('?' + qs
if qs
else '')
1106 qs
= self
.query_string()
1107 return reverse('rss_agenda') + ('?' + qs
if qs
else '')
1109 def get_email_alert_content(self
, results
):
1111 for evenement
in results
:
1112 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
1114 evenement
.get_absolute_url())
1115 content
+= u
' où ? : %s \n' % evenement
.lieu
1116 content
+= evenement
.debut
.strftime(u
' quand ? : %d/%m/%Y %H:%M \n')
1117 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
1118 content
+= u
' quoi ? : '
1119 content
+= '\n '.join(textwrap
.wrap(evenement
.description
))