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
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 auf
.django
.references
.models
import Region
, Pays
, Thematique
30 from settings
import CALENDRIER_URL
, SITE_ROOT_URL
, CONTACT_EMAIL
31 from lib
.calendrier
import combine
32 from lib
.recherche
import google_search
35 # Fonctionnalités communes à tous les query sets
37 class RandomQuerySetMixin(object):
38 """Mixin pour les modèles.
40 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
41 méthode pour récupérer des objets au hasard.
44 def random(self
, n
=1):
45 """Récupère aléatoirement un nombre donné d'objets."""
47 positions
= random
.sample(xrange(count
), min(n
, count
))
48 return [self
[p
] for p
in positions
]
51 class SEPQuerySet(models
.query
.QuerySet
, RandomQuerySetMixin
):
53 def _filter_date(self
, field
, min=None, max=None):
54 """Limite les résultats à ceux dont le champ ``field`` tombe entre
55 les dates ``min`` et ``max``."""
58 qs
= qs
.filter(**{field
+ '__gte': min})
61 field
+ '__lt': max + datetime
.timedelta(days
=1)
66 class SEPSphinxQuerySet(SphinxQuerySet
, RandomQuerySetMixin
):
67 """Fonctionnalités communes aux query sets de Sphinx."""
69 def __init__(self
, model
=None, index
=None, weights
=None):
70 SphinxQuerySet
.__init__(self
, model
=model
, index
=index
,
71 mode
='SPH_MATCH_EXTENDED2',
72 rankmode
='SPH_RANK_PROXIMITY_BM25',
75 def add_to_query(self
, query
):
76 """Ajoute une partie à la requête texte."""
78 # Assurons-nous qu'il y a un nombre pair de guillemets
79 if query
.count('"') % 2 != 0:
80 # Sinon, on enlève le dernier (faut choisir...)
82 query
= query
[:i
] + query
[i
+ 1:]
84 new_query
= smart_unicode(self
._query
) + ' ' + query \
85 if self
._query
else query
86 return self
.query(new_query
)
88 def search(self
, text
):
89 """Recherche ``text`` dans tous les champs."""
90 return self
.add_to_query('@* ' + text
)
92 def filter_discipline(self
, discipline
):
93 """Par défaut, le filtre par discipline cherche le nom de la
94 discipline dans tous les champs."""
95 return self
.search('"%s"' % discipline
.nom
)
97 def filter_region(self
, region
):
98 """Par défaut, le filtre par région cherche le nom de la région dans
100 return self
.search('"%s"' % region
.nom
)
102 def _filter_date(self
, field
, min=None, max=None):
103 """Limite les résultats à ceux dont le champ ``field`` tombe entre
104 les dates ``min`` et ``max``."""
107 qs
= qs
.filter(**{field
+ '__gte': min.toordinal() + 365})
109 qs
= qs
.filter(**{field
+ '__lte': max.toordinal() + 365})
112 def _get_sphinx_results(self
):
114 return SphinxQuerySet
._get_sphinx_results(self
)
116 # Essayons d'enlever les caractères qui peuvent poser problème.
117 for c
in '|!@()~/<=^$':
118 self
._query
= self
._query
.replace(c
, ' ')
120 return SphinxQuerySet
._get_sphinx_results(self
)
122 # Ça ne marche toujours pas. Enlevons les guillemets et les
125 self
._query
= self
._query
.replace(c
, ' ')
126 return SphinxQuerySet
._get_sphinx_results(self
)
129 class SEPManager(models
.Manager
):
130 """Lorsque les méthodes ``search``, ``filter_region`` et
131 ``filter_discipline`` sont appelées sur ce manager, le query set
132 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
134 def query(self
, query
):
135 return self
.get_sphinx_query_set().query(query
)
137 def add_to_query(self
, query
):
138 return self
.get_sphinx_query_set().add_to_query(query
)
140 def search(self
, text
):
141 return self
.get_sphinx_query_set().search(text
)
143 def filter_region(self
, region
):
144 return self
.get_sphinx_query_set().filter_region(region
)
146 def filter_discipline(self
, discipline
):
147 return self
.get_sphinx_query_set().filter_discipline(discipline
)
152 class Discipline(models
.Model
):
153 id = models
.IntegerField(primary_key
=True, db_column
='id_discipline')
154 nom
= models
.CharField(max_length
=765, db_column
='nom_discipline')
156 def __unicode__(self
):
160 db_table
= u
'discipline'
166 class SourceActualite(models
.Model
):
168 ('actu', 'Actualités'),
169 ('appels', "Appels d'offres"),
172 nom
= models
.CharField(max_length
=255)
173 url
= models
.CharField(max_length
=255, verbose_name
='URL', blank
=True)
174 type = models
.CharField(
175 max_length
=10, default
='actu', choices
=TYPE_CHOICES
179 verbose_name
= u
'fil RSS syndiqué'
180 verbose_name_plural
= u
'fils RSS syndiqués'
182 def __unicode__(self
,):
183 return u
"%s (%s)" % (self
.nom
, self
.get_type_display())
186 """Mise à jour du fil RSS."""
189 feed
= feedparser
.parse(self
.url
)
190 for entry
in feed
.entries
:
191 if Actualite
.all_objects
.filter(url
=entry
.link
).count() == 0:
192 ts
= entry
.get('updated_parsed')
193 date
= datetime
.date(ts
.tm_year
, ts
.tm_mon
, ts
.tm_mday
) \
194 if ts
else datetime
.date
.today()
195 self
.actualites
.create(
196 titre
=entry
.title
, texte
=entry
.summary_detail
.value
,
197 url
=entry
.link
, date
=date
201 class ActualiteQuerySet(SEPQuerySet
):
203 def filter_date(self
, min=None, max=None):
204 return self
._filter_date('date', min=min, max=max)
206 def filter_type(self
, type):
207 return self
.filter(source__type
=type)
210 class ActualiteSphinxQuerySet(SEPSphinxQuerySet
):
211 TYPE_CODES
= {'actu': 1, 'appels': 2}
213 def __init__(self
, model
=None):
214 SEPSphinxQuerySet
.__init__(
215 self
, model
=model
, index
='savoirsenpartage_actualites',
216 weights
=dict(titre
=3)
219 def filter_date(self
, min=None, max=None):
220 return self
._filter_date('date', min=min, max=max)
222 def filter_type(self
, type):
223 return self
.filter(type=self
.TYPE_CODES
[type])
225 def filter_region(self
, region
):
226 return self
.filter(region_ids
=region
.id)
228 def filter_discipline(self
, discipline
):
229 return self
.filter(discipline_ids
=discipline
.id)
232 class ActualiteManager(SEPManager
):
234 def get_query_set(self
):
235 return ActualiteQuerySet(self
.model
).filter(visible
=True)
237 def get_sphinx_query_set(self
):
238 return ActualiteSphinxQuerySet(self
.model
).order_by('-date')
240 def filter_date(self
, min=None, max=None):
241 return self
.get_query_set().filter_date(min=min, max=max)
243 def filter_type(self
, type):
244 return self
.get_query_set().filter_type(type)
247 class Actualite(models
.Model
):
248 id = models
.AutoField(primary_key
=True, db_column
='id_actualite')
249 titre
= models
.CharField(max_length
=765, db_column
='titre_actualite')
250 texte
= models
.TextField(db_column
='texte_actualite')
251 url
= models
.CharField(max_length
=765, db_column
='url_actualite')
252 date
= models
.DateField(db_column
='date_actualite')
253 visible
= models
.BooleanField(db_column
='visible_actualite', default
=False)
254 ancienid
= models
.IntegerField(
255 db_column
='ancienId_actualite', blank
=True, null
=True
257 source
= models
.ForeignKey(SourceActualite
, related_name
='actualites')
258 disciplines
= models
.ManyToManyField(
259 Discipline
, blank
=True, related_name
="actualites"
261 regions
= models
.ManyToManyField(
262 Region
, blank
=True, related_name
="actualites",
263 verbose_name
='régions'
266 objects
= ActualiteManager()
267 all_objects
= models
.Manager()
270 db_table
= u
'actualite'
273 def __unicode__(self
):
274 return "%s" % (self
.titre
)
276 def get_absolute_url(self
):
277 return reverse('actualite', kwargs
={'id': self
.id})
279 def assigner_disciplines(self
, disciplines
):
280 self
.disciplines
.add(*disciplines
)
282 def assigner_regions(self
, regions
):
283 self
.regions
.add(*regions
)
286 class ActualiteVoir(Actualite
):
290 verbose_name
= 'actualité (visualisation)'
291 verbose_name_plural
= 'actualités (visualisation)'
296 class EvenementQuerySet(SEPQuerySet
):
298 def filter_type(self
, type):
299 return self
.filter(type=type)
301 def filter_debut(self
, min=None, max=None):
302 return self
._filter_date('debut', min=min, max=max)
304 def filter_date_modification(self
, min=None, max=None):
305 return self
._filter_date('date_modification', min=min, max=max)
308 class EvenementSphinxQuerySet(SEPSphinxQuerySet
):
310 def __init__(self
, model
=None):
311 SEPSphinxQuerySet
.__init__(
312 self
, model
=model
, index
='savoirsenpartage_evenements',
313 weights
=dict(titre
=3)
316 def filter_type(self
, type):
317 return self
.add_to_query('@type "%s"' % type)
319 def filter_debut(self
, min=None, max=None):
320 return self
._filter_date('debut', min=min, max=max)
322 def filter_date_modification(self
, min=None, max=None):
323 return self
._filter_date('date_modification', min=min, max=max)
325 def filter_region(self
, region
):
326 return self
.add_to_query('@regions "%s"' % region
.nom
)
328 def filter_discipline(self
, discipline
):
329 return self
.add_to_query('@disciplines "%s"' % discipline
.nom
)
332 class EvenementManager(SEPManager
):
334 def get_query_set(self
):
335 return EvenementQuerySet(self
.model
).filter(approuve
=True)
337 def get_sphinx_query_set(self
):
338 return EvenementSphinxQuerySet(self
.model
).order_by('-debut')
340 def filter_type(self
, type):
341 return self
.get_query_set().filter_type(type)
343 def filter_debut(self
, min=None, max=None):
344 return self
.get_query_set().filter_debut(min=min, max=max)
346 def filter_date_modification(self
, min=None, max=None):
347 return self
.get_query_set().filter_date_modification(min=min, max=max)
350 def build_time_zone_choices(pays
=None):
351 timezones
= pytz
.country_timezones
[pays
] if pays
else pytz
.common_timezones
353 now
= datetime
.datetime
.now()
354 for tzname
in timezones
:
355 tz
= pytz
.timezone(tzname
)
356 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
358 offset
= tz
.utcoffset(now
)
359 except (AmbiguousTimeError
, NonExistentTimeError
):
360 # oups. On est en train de changer d'heure. Ça devrait être fini
362 offset
= tz
.utcoffset(now
+ datetime
.timedelta(days
=1))
363 seconds
= offset
.seconds
+ offset
.days
* 86400
364 (hours
, minutes
) = divmod(seconds
// 60, 60)
365 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) \
366 if minutes
else 'UTC%+d' % hours
367 result
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
369 return [(x
[1], x
[2]) for x
in result
]
372 class Evenement(models
.Model
):
373 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
374 (u
'Conférence', u
'Conférence'),
375 (u
'Appel à contribution', u
'Appel à contribution'),
376 (u
'Journée d\'étude', u
'Journée d\'étude'),
377 (u
'Autre', u
'Autre'))
378 TIME_ZONE_CHOICES
= build_time_zone_choices()
380 uid
= models
.CharField(max_length
=255, default
=str(uuid
.uuid1()))
381 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
382 titre
= models
.CharField(max_length
=255)
383 discipline
= models
.ForeignKey(
384 'Discipline', related_name
="discipline",
385 blank
=True, null
=True
387 discipline_secondaire
= models
.ForeignKey(
388 'Discipline', related_name
="discipline_secondaire",
389 verbose_name
=u
"discipline secondaire", blank
=True, null
=True
391 mots_cles
= models
.TextField('Mots-Clés', blank
=True, null
=True)
392 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
393 adresse
= models
.TextField()
394 ville
= models
.CharField(max_length
=100)
395 pays
= models
.ForeignKey(
396 Pays
, to_field
='code', null
=True, related_name
='evenements'
398 debut
= models
.DateTimeField(default
=datetime
.datetime
.now
)
399 fin
= models
.DateTimeField(default
=datetime
.datetime
.now
)
400 fuseau
= models
.CharField(
401 max_length
=100, choices
=TIME_ZONE_CHOICES
,
402 verbose_name
='fuseau horaire'
404 description
= models
.TextField()
405 contact
= models
.TextField(null
=True) # champ obsolète
406 prenom
= models
.CharField('prénom', max_length
=100)
407 nom
= models
.CharField(max_length
=100)
408 courriel
= models
.EmailField()
409 url
= models
.CharField(max_length
=255, blank
=True, null
=True)
410 piece_jointe
= models
.FileField(
411 upload_to
='agenda/pj', blank
=True, verbose_name
='pièce jointe'
413 regions
= models
.ManyToManyField(
414 Region
, blank
=True, related_name
="evenements",
415 verbose_name
='régions additionnelles',
416 help_text
="On considère d'emblée que l'évènement se déroule dans la "
417 "région dans laquelle se trouve le pays indiqué plus haut. Il est "
418 "possible de désigner ici des régions additionnelles."
420 date_modification
= models
.DateTimeField(
421 editable
=False, auto_now
=True, null
=True
424 objects
= EvenementManager()
425 all_objects
= models
.Manager()
428 ordering
= ['-debut']
429 verbose_name
= u
'évènement'
430 verbose_name_plural
= u
'évènements'
432 def __unicode__(self
):
433 return "[%s] %s" % (self
.uid
, self
.titre
)
435 def get_absolute_url(self
):
436 return reverse('evenement', kwargs
={'id': self
.id})
438 def duration_display(self
):
439 delta
= self
.fin
- self
.debut
440 minutes
, seconds
= divmod(delta
.seconds
, 60)
441 hours
, minutes
= divmod(minutes
, 60)
445 parts
.append('1 jour')
447 parts
.append('%d jours' % days
)
449 parts
.append('1 heure')
451 parts
.append('%d heures' % hours
)
453 parts
.append('1 minute')
455 parts
.append('%d minutes' % minutes
)
456 return ' '.join(parts
)
458 def piece_jointe_display(self
):
459 return self
.piece_jointe
and os
.path
.basename(self
.piece_jointe
.name
)
461 def courriel_display(self
):
462 return self
.courriel
.replace(u
'@', u
' (à) ')
468 bits
.append(self
.adresse
)
470 bits
.append(self
.ville
)
472 bits
.append(self
.pays
.nom
)
473 return ', '.join(bits
)
476 from django
.core
.exceptions
import ValidationError
477 if self
.debut
> self
.fin
:
478 raise ValidationError(
479 'La date de fin ne doit pas être antérieure à la date de début'
482 def save(self
, *args
, **kwargs
):
484 Sauvegarde l'objet dans django et le synchronise avec caldav s'il a
487 self
.contact
= '' # Vider ce champ obsolète à la première occasion...
489 super(Evenement
, self
).save(*args
, **kwargs
)
492 # methodes de commnunications avec CALDAV
494 """Retourne l'evenement django sous forme d'objet icalendar"""
495 cal
= vobject
.iCalendar()
498 # fournit son propre uid
499 if self
.uid
in [None, ""]:
500 self
.uid
= str(uuid
.uuid1())
502 cal
.vevent
.add('uid').value
= self
.uid
504 cal
.vevent
.add('summary').value
= self
.titre
506 if self
.mots_cles
is None:
509 kw
= self
.mots_cles
.split(",")
512 kw
.append(self
.discipline
.nom
)
513 kw
.append(self
.discipline_secondaire
.nom
)
518 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
520 cal
.vevent
.add('x-auf-keywords').value
= k
522 description
= self
.description
524 if len(self
.description
) > 0:
526 description
+= u
"Mots-clés: " + ", ".join(kw
)
528 cal
.vevent
.add('dtstart').value
= \
529 combine(self
.debut
, pytz
.timezone(self
.fuseau
))
530 cal
.vevent
.add('dtend').value
= \
531 combine(self
.fin
, pytz
.timezone(self
.fuseau
))
532 cal
.vevent
.add('created').value
= \
533 combine(datetime
.datetime
.now(), "UTC")
534 cal
.vevent
.add('dtstamp').value
= \
535 combine(datetime
.datetime
.now(), "UTC")
536 if len(description
) > 0:
537 cal
.vevent
.add('description').value
= description
538 if len(self
.contact
) > 0:
539 cal
.vevent
.add('contact').value
= self
.contact
540 if len(self
.url
) > 0:
541 cal
.vevent
.add('url').value
= self
.url
542 cal
.vevent
.add('location').value
= ', '.join(
543 x
for x
in [self
.adresse
, self
.ville
, self
.pays
.nom
] if x
545 if self
.piece_jointe
:
546 url
= self
.piece_jointe
.url
547 if not url
.startswith('http://'):
548 url
= SITE_ROOT_URL
+ url
549 cal
.vevent
.add('attach').value
= url
552 def update_vevent(self
,):
553 """Essaie de créer l'évènement sur le serveur ical.
554 En cas de succès, l'évènement local devient donc inactif et approuvé"""
557 event
= self
.as_ical()
558 client
= caldav
.DAVClient(CALENDRIER_URL
)
559 cal
= caldav
.Calendar(client
, url
=CALENDRIER_URL
)
561 client
, parent
=cal
, data
=event
.serialize(), id=self
.uid
565 self
.approuve
= False
567 def delete_vevent(self
,):
568 """Supprime l'evenement sur le serveur caldav"""
571 client
= caldav
.DAVClient(CALENDRIER_URL
)
572 cal
= caldav
.Calendar(client
, url
=CALENDRIER_URL
)
573 e
= cal
.event(self
.uid
)
575 except error
.NotFoundError
:
578 def assigner_regions(self
, regions
):
579 self
.regions
.add(*regions
)
581 def assigner_disciplines(self
, disciplines
):
582 if len(disciplines
) == 1:
584 self
.discipline_secondaire
= disciplines
[0]
586 self
.discipline
= disciplines
[0]
587 elif len(disciplines
) >= 2:
588 self
.discipline
= disciplines
[0]
589 self
.discipline_secondaire
= disciplines
[1]
592 def delete_vevent(sender
, instance
, *args
, **kwargs
):
593 # Surcharge du comportement de suppression
594 # La méthode de connexion par signals est préférable à surcharger la
595 # méthode delete() car dans le cas de la suppression par lots, cell-ci
597 instance
.delete_vevent()
598 pre_delete
.connect(delete_vevent
, sender
=Evenement
)
601 class EvenementVoir(Evenement
):
605 verbose_name
= 'événement (visualisation)'
606 verbose_name_plural
= 'événement (visualisation)'
611 class ListSet(models
.Model
):
612 spec
= models
.CharField(primary_key
=True, max_length
=255)
613 name
= models
.CharField(max_length
=255)
614 server
= models
.CharField(max_length
=255)
615 validated
= models
.BooleanField(default
=True)
617 def __unicode__(self
,):
621 class RecordCategorie(models
.Model
):
622 nom
= models
.CharField(max_length
=255)
625 verbose_name
= 'catégorie ressource'
626 verbose_name_plural
= 'catégories ressources'
628 def __unicode__(self
):
632 class RecordQuerySet(SEPQuerySet
):
634 def filter_modified(self
, min=None, max=None):
635 return self
._filter_date('modified', min=min, max=max)
638 class RecordSphinxQuerySet(SEPSphinxQuerySet
):
640 def __init__(self
, model
=None):
641 SEPSphinxQuerySet
.__init__(
642 self
, model
=model
, index
='savoirsenpartage_ressources',
643 weights
=dict(title
=3)
646 def filter_modified(self
, min=None, max=None):
647 return self
._filter_date('modified', min=min, max=max)
649 def filter_region(self
, region
):
650 return self
.filter(region_ids
=region
.id)
652 def filter_discipline(self
, discipline
):
653 return self
.filter(discipline_ids
=discipline
.id)
656 class RecordManager(SEPManager
):
658 def get_query_set(self
):
659 """Ne garder que les ressources validées et qui sont soit dans aucun
660 listset ou au moins dans un listset validé."""
661 qs
= RecordQuerySet(self
.model
)
662 qs
= qs
.filter(validated
=True)
663 qs
= qs
.filter(Q(listsets__isnull
=True) |
Q(listsets__validated
=True))
666 def get_sphinx_query_set(self
):
667 return RecordSphinxQuerySet(self
.model
)
669 def filter_modified(self
, min=None, max=None):
670 return self
.get_query_set().filter_modified(min=min, max=max)
673 class Record(models
.Model
):
675 #fonctionnement interne
676 id = models
.AutoField(primary_key
=True)
677 server
= models
.CharField(max_length
=255, verbose_name
=u
'serveur')
678 last_update
= models
.CharField(max_length
=255)
679 last_checksum
= models
.CharField(max_length
=255)
680 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
683 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
684 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
685 description
= models
.TextField(null
=True, blank
=True)
686 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
687 identifier
= models
.CharField(
688 max_length
=255, null
=True, blank
=True, unique
=True
690 uri
= models
.CharField(max_length
=255, null
=True, blank
=True, unique
=True)
691 source
= models
.TextField(null
=True, blank
=True)
692 contributor
= models
.TextField(null
=True, blank
=True)
693 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
694 publisher
= models
.TextField(null
=True, blank
=True)
695 type = models
.TextField(null
=True, blank
=True)
696 format
= models
.TextField(null
=True, blank
=True)
697 language
= models
.TextField(null
=True, blank
=True)
699 listsets
= models
.ManyToManyField(ListSet
, null
=True, blank
=True)
701 #SEP 2 (aucune données récoltées)
702 alt_title
= models
.TextField(null
=True, blank
=True)
703 abstract
= models
.TextField(null
=True, blank
=True)
704 creation
= models
.CharField(max_length
=255, null
=True, blank
=True)
705 issued
= models
.CharField(max_length
=255, null
=True, blank
=True)
706 isbn
= models
.TextField(null
=True, blank
=True)
707 orig_lang
= models
.TextField(null
=True, blank
=True)
709 categorie
= models
.ForeignKey(
710 RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie'
713 # Metadata AUF multivaluées
714 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
715 thematiques
= models
.ManyToManyField(
716 Thematique
, blank
=True, verbose_name
='thématiques'
718 pays
= models
.ManyToManyField(Pays
, blank
=True)
719 regions
= models
.ManyToManyField(
720 Region
, blank
=True, verbose_name
='régions'
724 objects
= RecordManager()
725 all_objects
= models
.Manager()
728 verbose_name
= 'ressource'
730 def __unicode__(self
):
731 return "[%s] %s" % (self
.server
, self
.title
)
733 def get_absolute_url(self
):
734 return reverse('ressource', kwargs
={'id': self
.id})
736 def getServeurURL(self
):
737 """Retourne l'URL du serveur de provenance"""
738 return RESOURCES
[self
.server
]['url']
740 def est_complet(self
):
741 """teste si le record à toutes les données obligatoires"""
742 return self
.disciplines
.count() > 0 and \
743 self
.thematiques
.count() > 0 and \
744 self
.pays
.count() > 0 and \
745 self
.regions
.count() > 0
747 def assigner_regions(self
, regions
):
748 self
.regions
.add(*regions
)
750 def assigner_disciplines(self
, disciplines
):
751 self
.disciplines
.add(*disciplines
)
754 class RecordEdit(Record
):
758 verbose_name
= 'ressource (édition)'
759 verbose_name_plural
= 'ressources (édition)'
762 class Serveur(models
.Model
):
763 """Identification d'un serveur d'ou proviennent les références"""
764 nom
= models
.CharField(primary_key
=True, max_length
=255)
766 def __unicode__(self
,):
769 def conf_2_db(self
,):
770 for k
in RESOURCES
.keys():
771 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
776 class Profile(models
.Model
):
777 user
= models
.ForeignKey(User
, unique
=True)
778 serveurs
= models
.ManyToManyField(Serveur
, null
=True, blank
=True)
781 class HarvestLog(models
.Model
):
782 context
= models
.CharField(max_length
=255)
783 name
= models
.CharField(max_length
=255)
784 date
= models
.DateTimeField(auto_now
=True)
785 added
= models
.IntegerField(null
=True, blank
=True)
786 updated
= models
.IntegerField(null
=True, blank
=True)
787 processed
= models
.IntegerField(null
=True, blank
=True)
788 record
= models
.ForeignKey(Record
, null
=True, blank
=True)
792 logger
= HarvestLog()
793 if 'record_id' in message
:
794 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
795 del(message
['record_id'])
797 for k
, v
in message
.items():
798 setattr(logger
, k
, v
)
804 class PageStatique(models
.Model
):
805 id = models
.CharField(max_length
=32, primary_key
=True)
806 titre
= models
.CharField(max_length
=100)
807 contenu
= models
.TextField()
810 verbose_name_plural
= 'pages statiques'
815 class GlobalSearchResults(object):
817 def __init__(self
, actualites
=None, appels
=None, evenements
=None,
818 ressources
=None, chercheurs
=None, groupes
=None,
819 sites
=None, sites_auf
=None):
820 self
.actualites
= actualites
822 self
.evenements
= evenements
823 self
.ressources
= ressources
824 self
.chercheurs
= chercheurs
825 self
.groupes
= groupes
827 self
.sites_auf
= sites_auf
829 def __nonzero__(self
):
830 return bool(self
.actualites
or self
.appels
or self
.evenements
or
831 self
.ressources
or self
.chercheurs
or self
.groupes
or
832 self
.sites
or self
.sites_auf
)
835 class Search(models
.Model
):
836 user
= models
.ForeignKey(User
, editable
=False)
837 content_type
= models
.ForeignKey(ContentType
, editable
=False)
838 nom
= models
.CharField(max_length
=100, verbose_name
="nom de la recherche")
839 alerte_courriel
= models
.BooleanField(
840 verbose_name
="Envoyer une alerte courriel"
842 derniere_alerte
= models
.DateField(
843 verbose_name
="Date d'envoi de la dernière alerte courriel",
844 null
=True, editable
=False
846 q
= models
.CharField(
847 max_length
=255, blank
=True, verbose_name
="dans tous les champs"
849 discipline
= models
.ForeignKey(Discipline
, blank
=True, null
=True)
850 region
= models
.ForeignKey(
851 Region
, blank
=True, null
=True, verbose_name
='région',
852 help_text
="La région est ici définie au sens, non strictement "
853 "géographique, du Bureau régional de l'AUF de référence."
856 def query_string(self
):
858 for field
in self
._meta
.fields
:
859 if field
.name
in ['id', 'user', 'nom', 'search_ptr',
862 value
= getattr(self
, field
.column
)
864 if isinstance(value
, datetime
.date
):
865 params
[field
.name
] = value
.strftime('%d/%m/%Y')
867 params
[field
.name
] = smart_str(value
)
868 return urlencode(params
)
871 verbose_name
= 'recherche transversale'
872 verbose_name_plural
= "recherches transversales"
874 def __unicode__(self
):
878 if self
.alerte_courriel
:
880 original_search
= Search
.objects
.get(id=self
.id)
881 if not original_search
.alerte_courriel
:
882 # On a nouvellement activé l'alerte courriel. Notons la
884 self
.derniere_alerte
= \
885 datetime
.date
.today() - datetime
.timedelta(days
=1)
886 except Search
.DoesNotExist
:
887 self
.derniere_alerte
= \
888 datetime
.date
.today() - datetime
.timedelta(days
=1)
889 if (not self
.content_type_id
):
890 self
.content_type
= ContentType
.objects
.get_for_model(
893 super(Search
, self
).save()
895 def as_leaf_class(self
):
896 content_type
= self
.content_type
897 model
= content_type
.model_class()
900 return model
.objects
.get(id=self
.id)
902 def run(self
, min_date
=None, max_date
=None):
903 from chercheurs
.models
import Chercheur
, Groupe
904 from sitotheque
.models
import Site
906 actualites
= Actualite
.objects
907 evenements
= Evenement
.objects
908 ressources
= Record
.objects
909 chercheurs
= Chercheur
.objects
910 groupes
= Groupe
.objects
913 actualites
= actualites
.search(self
.q
)
914 evenements
= evenements
.search(self
.q
)
915 ressources
= ressources
.search(self
.q
)
916 chercheurs
= chercheurs
.search(self
.q
)
917 groupes
= groupes
.search(self
.q
)
918 sites
= sites
.search(self
.q
)
920 actualites
= actualites
.filter_discipline(self
.discipline
)
921 evenements
= evenements
.filter_discipline(self
.discipline
)
922 ressources
= ressources
.filter_discipline(self
.discipline
)
923 chercheurs
= chercheurs
.filter_discipline(self
.discipline
)
924 sites
= sites
.filter_discipline(self
.discipline
)
926 actualites
= actualites
.filter_region(self
.region
)
927 evenements
= evenements
.filter_region(self
.region
)
928 ressources
= ressources
.filter_region(self
.region
)
929 chercheurs
= chercheurs
.filter_region(self
.region
)
930 sites
= sites
.filter_region(self
.region
)
932 actualites
= actualites
.filter_date(min=min_date
)
933 evenements
= evenements
.filter_date_modification(min=min_date
)
934 ressources
= ressources
.filter_modified(min=min_date
)
935 chercheurs
= chercheurs
.filter_date_modification(min=min_date
)
936 sites
= sites
.filter_date_maj(min=min_date
)
938 actualites
= actualites
.filter_date(max=max_date
)
939 evenements
= evenements
.filter_date_modification(max=max_date
)
940 ressources
= ressources
.filter_modified(max=max_date
)
941 chercheurs
= chercheurs
.filter_date_modification(max=max_date
)
942 sites
= sites
.filter_date_maj(max=max_date
)
945 sites_auf
= google_search(0, self
.q
)['results']
949 return GlobalSearchResults(
950 actualites
=actualites
.order_by('-date').filter_type('actu'),
951 appels
=actualites
.order_by('-date').filter_type('appels'),
952 evenements
=evenements
.order_by('-debut'),
953 ressources
=ressources
.order_by('-id'),
954 chercheurs
=chercheurs
.order_by('-date_modification'),
955 groupes
=groupes
.order_by('nom'),
956 sites
=sites
.order_by('-date_maj'),
962 if self
.content_type
.model
!= 'search':
963 obj
= self
.content_type
.get_object_for_this_type(pk
=self
.pk
)
968 url
+= '/discipline/%d' % self
.discipline
.id
970 url
+= '/region/%d' % self
.region
.id
973 url
+= '?' + urlencode({'q': smart_str(self
.q
)})
979 def send_email_alert(self
):
980 """Envoie une alerte courriel correspondant à cette recherche"""
981 yesterday
= datetime
.date
.today() - datetime
.timedelta(days
=1)
982 if self
.derniere_alerte
is not None:
983 results
= self
.as_leaf_class().run(
984 min_date
=self
.derniere_alerte
, max_date
=yesterday
987 subject
= 'Savoirs en partage - ' + self
.nom
988 from_email
= CONTACT_EMAIL
989 to_email
= self
.user
.email
990 text_content
= u
'Voici les derniers résultats ' \
991 u
'correspondant à votre recherche sauvegardée.\n\n'
992 text_content
+= self
.as_leaf_class() \
993 .get_email_alert_content(results
)
996 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
997 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (
998 SITE_ROOT_URL
, reverse('recherches')
1001 '<div style="font-family: Arial, sans-serif">\n' + \
1002 markdown(smart_str(text_content
)) + '</div>\n'
1003 msg
= EmailMultiAlternatives(
1004 subject
, text_content
, from_email
, [to_email
]
1006 msg
.attach_alternative(html_content
, "text/html")
1008 self
.derniere_alerte
= yesterday
1011 def get_email_alert_content(self
, results
):
1013 if results
.chercheurs
:
1014 content
+= u
'\n### Nouveaux chercheurs\n\n'
1015 for chercheur
in results
.chercheurs
:
1016 content
+= u
'- [%s %s](%s%s) \n' % (
1017 chercheur
.nom
.upper(), chercheur
.prenom
, SITE_ROOT_URL
,
1018 chercheur
.get_absolute_url()
1020 content
+= u
' %s\n\n' % chercheur
.etablissement_display
1021 if results
.ressources
:
1022 content
+= u
'\n### Nouvelles ressources\n\n'
1023 for ressource
in results
.ressources
:
1024 content
+= u
'- [%s](%s%s)\n\n' % (
1025 ressource
.title
, SITE_ROOT_URL
,
1026 ressource
.get_absolute_url()
1028 if ressource
.description
:
1032 for line
in textwrap
.wrap(ressource
.description
)
1036 if results
.actualites
:
1037 content
+= u
'\n### Nouvelles actualités\n\n'
1038 for actualite
in results
.actualites
:
1039 content
+= u
'- [%s](%s%s)\n\n' % (
1040 actualite
.titre
, SITE_ROOT_URL
,
1041 actualite
.get_absolute_url()
1047 for line
in textwrap
.wrap(actualite
.texte
)
1051 content
+= u
"\n### Nouveaux appels d'offres\n\n"
1052 for appel
in results
.appels
:
1053 content
+= u
'- [%s](%s%s)\n\n' % (appel
.titre
,
1055 appel
.get_absolute_url())
1060 for line
in textwrap
.wrap(appel
.texte
)
1063 if results
.evenements
:
1064 content
+= u
"\n### Nouveaux évènements\n\n"
1065 for evenement
in results
.evenements
:
1066 content
+= u
'- [%s](%s%s) \n' % (
1067 evenement
.titre
, SITE_ROOT_URL
,
1068 evenement
.get_absolute_url()
1070 content
+= u
' où ? : %s \n' % evenement
.lieu
1071 content
+= evenement
.debut
.strftime(
1072 ' quand ? : %d/%m/%Y %H:%M \n'
1074 content
+= u
' durée ? : %s\n\n' % \
1075 evenement
.duration_display()
1076 content
+= u
' quoi ? : '
1077 content
+= '\n '.join(
1078 textwrap
.wrap(evenement
.description
)
1082 content
+= u
"\n### Nouveaux sites\n\n"
1083 for site
in results
.sites
:
1084 content
+= u
'- [%s](%s%s)\n\n' % (site
.titre
,
1086 site
.get_absolute_url())
1087 if site
.description
:
1091 for line
in textwrap
.wrap(site
.description
)
1097 class RessourceSearch(Search
):
1098 auteur
= models
.CharField(
1099 max_length
=100, blank
=True, verbose_name
="auteur ou contributeur"
1101 titre
= models
.CharField(max_length
=100, blank
=True)
1102 sujet
= models
.CharField(max_length
=100, blank
=True)
1103 publisher
= models
.CharField(
1104 max_length
=100, blank
=True, verbose_name
="éditeur"
1106 categorie
= models
.ForeignKey(
1107 RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie'
1111 verbose_name
= 'recherche de ressources'
1112 verbose_name_plural
= "recherches de ressources"
1114 def run(self
, min_date
=None, max_date
=None):
1115 results
= Record
.objects
1117 results
= results
.search(self
.q
)
1119 results
= results
.add_to_query(
1120 '@(creator,contributor) ' + self
.auteur
1123 results
= results
.add_to_query('@title ' + self
.titre
)
1125 results
= results
.add_to_query('@subject ' + self
.sujet
)
1127 results
= results
.add_to_query('@publisher ' + self
.publisher
)
1129 results
= results
.add_to_query('@categorie %s' % self
.categorie
.id)
1131 results
= results
.filter_discipline(self
.discipline
)
1133 results
= results
.filter_region(self
.region
)
1135 results
= results
.filter_modified(min=min_date
)
1137 results
= results
.filter_modified(max=max_date
)
1139 """Montrer les résultats les plus récents si on n'a pas fait
1140 une recherche par mots-clés."""
1141 results
= results
.order_by('-modified')
1142 return results
.all()
1145 qs
= self
.query_string()
1146 return reverse('ressources') + ('?' + qs
if qs
else '')
1149 qs
= self
.query_string()
1150 return reverse('rss_ressources') + ('?' + qs
if qs
else '')
1152 def get_email_alert_content(self
, results
):
1154 for ressource
in results
:
1155 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
1157 ressource
.get_absolute_url())
1158 if ressource
.description
:
1162 for line
in textwrap
.wrap(ressource
.description
)
1168 class ActualiteSearchBase(Search
):
1169 date_min
= models
.DateField(
1170 blank
=True, null
=True, verbose_name
="depuis le"
1172 date_max
= models
.DateField(
1173 blank
=True, null
=True, verbose_name
="jusqu'au"
1179 def run(self
, min_date
=None, max_date
=None):
1180 results
= Actualite
.objects
1182 results
= results
.search(self
.q
)
1184 results
= results
.filter_discipline(self
.discipline
)
1186 results
= results
.filter_region(self
.region
)
1188 results
= results
.filter_date(min=self
.date_min
)
1190 results
= results
.filter_date(max=self
.date_max
)
1192 results
= results
.filter_date(min=min_date
)
1194 results
= results
.filter_date(max=max_date
)
1195 return results
.all()
1197 def get_email_alert_content(self
, results
):
1199 for actualite
in results
:
1200 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
1202 actualite
.get_absolute_url())
1207 for line
in textwrap
.wrap(actualite
.texte
)
1213 class ActualiteSearch(ActualiteSearchBase
):
1216 verbose_name
= "recherche d'actualités"
1217 verbose_name_plural
= "recherches d'actualités"
1219 def run(self
, min_date
=None, max_date
=None):
1220 return super(ActualiteSearch
, self
) \
1221 .run(min_date
=min_date
, max_date
=max_date
) \
1222 .filter_type('actu')
1225 qs
= self
.query_string()
1226 return reverse('actualites') + ('?' + qs
if qs
else '')
1229 qs
= self
.query_string()
1230 return reverse('rss_actualites') + ('?' + qs
if qs
else '')
1233 class AppelSearch(ActualiteSearchBase
):
1236 verbose_name
= "recherche d'appels d'offres"
1237 verbose_name_plural
= "recherches d'appels d'offres"
1239 def run(self
, min_date
=None, max_date
=None):
1240 return super(AppelSearch
, self
) \
1241 .run(min_date
=min_date
, max_date
=max_date
) \
1242 .filter_type('appels')
1245 qs
= self
.query_string()
1246 return reverse('appels') + ('?' + qs
if qs
else '')
1249 qs
= self
.query_string()
1250 return reverse('rss_appels') + ('?' + qs
if qs
else '')
1253 class EvenementSearch(Search
):
1254 titre
= models
.CharField(
1255 max_length
=100, blank
=True, verbose_name
="Intitulé"
1257 type = models
.CharField(
1258 max_length
=100, blank
=True, choices
=Evenement
.TYPE_CHOICES
1260 date_min
= models
.DateField(
1261 blank
=True, null
=True, verbose_name
="depuis le"
1263 date_max
= models
.DateField(
1264 blank
=True, null
=True, verbose_name
="jusqu'au"
1268 verbose_name
= "recherche d'évènements"
1269 verbose_name_plural
= "recherches d'évènements"
1271 def run(self
, min_date
=None, max_date
=None):
1272 results
= Evenement
.objects
1274 results
= results
.search(self
.q
)
1276 results
= results
.add_to_query('@titre ' + self
.titre
)
1278 results
= results
.filter_discipline(self
.discipline
)
1280 results
= results
.filter_region(self
.region
)
1282 results
= results
.filter_type(self
.type)
1284 results
= results
.filter_debut(min=self
.date_min
)
1286 results
= results
.filter_debut(max=self
.date_max
)
1288 results
= results
.filter_date_modification(min=min_date
)
1290 results
= results
.filter_date_modification(max=max_date
)
1291 return results
.all()
1294 qs
= self
.query_string()
1295 return reverse('agenda') + ('?' + qs
if qs
else '')
1298 qs
= self
.query_string()
1299 return reverse('rss_agenda') + ('?' + qs
if qs
else '')
1301 def get_email_alert_content(self
, results
):
1303 for evenement
in results
:
1304 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
1306 evenement
.get_absolute_url())
1307 content
+= u
' où ? : %s \n' % evenement
.lieu
1308 content
+= evenement
.debut
.strftime(
1309 ' quand ? : %d/%m/%Y %H:%M \n'
1311 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
1312 content
+= u
' quoi ? : '
1313 content
+= '\n '.join(
1314 textwrap
.wrap(evenement
.description
)