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
177 region
= models
.ForeignKey(Region
, null
=True, blank
=True, verbose_name
='région')
180 verbose_name
= u
'fil RSS syndiqué'
181 verbose_name_plural
= u
'fils RSS syndiqués'
183 def __unicode__(self
,):
184 return u
"%s (%s)" % (self
.nom
, self
.get_type_display())
187 """Mise à jour du fil RSS."""
190 feed
= feedparser
.parse(self
.url
)
191 for entry
in feed
.entries
:
193 x
in entry
for x
in ('link', 'published_parsed', 'title')
196 if Actualite
.all_objects
.filter(url
=entry
.link
).count() == 0:
197 ts
= entry
.get('published_parsed')
198 date
= datetime
.date(ts
.tm_year
, ts
.tm_mon
, ts
.tm_mday
) \
199 if ts
else datetime
.date
.today()
200 texte
= entry
.summary_detail
.value \
201 if 'summary_detail' in entry
else ''
202 actualite
= self
.actualites
.create(
203 titre
=entry
.title
, texte
=texte
, url
=entry
.link
,
207 actualite
.regions
.add(self
.region
)
210 class ActualiteQuerySet(SEPQuerySet
):
212 def filter_date(self
, min=None, max=None):
213 return self
._filter_date('date', min=min, max=max)
215 def filter_type(self
, type):
216 return self
.filter(source__type
=type)
219 class ActualiteSphinxQuerySet(SEPSphinxQuerySet
):
220 TYPE_CODES
= {'actu': 1, 'appels': 2}
222 def __init__(self
, model
=None):
223 SEPSphinxQuerySet
.__init__(
224 self
, model
=model
, index
='savoirsenpartage_actualites',
225 weights
=dict(titre
=3)
228 def filter_date(self
, min=None, max=None):
229 return self
._filter_date('date', min=min, max=max)
231 def filter_type(self
, type):
232 return self
.filter(type=self
.TYPE_CODES
[type])
234 def filter_region(self
, region
):
235 return self
.filter(region_ids
=region
.id)
237 def filter_discipline(self
, discipline
):
238 return self
.filter(discipline_ids
=discipline
.id)
241 class ActualiteManager(SEPManager
):
243 def get_query_set(self
):
244 return ActualiteQuerySet(self
.model
).filter(visible
=True)
246 def get_sphinx_query_set(self
):
247 return ActualiteSphinxQuerySet(self
.model
).order_by('-date')
249 def filter_date(self
, min=None, max=None):
250 return self
.get_query_set().filter_date(min=min, max=max)
252 def filter_type(self
, type):
253 return self
.get_query_set().filter_type(type)
256 class Actualite(models
.Model
):
257 id = models
.AutoField(primary_key
=True, db_column
='id_actualite')
258 titre
= models
.CharField(max_length
=765, db_column
='titre_actualite')
259 texte
= models
.TextField(db_column
='texte_actualite')
260 url
= models
.CharField(max_length
=765, db_column
='url_actualite')
261 date
= models
.DateField(db_column
='date_actualite')
262 visible
= models
.BooleanField(db_column
='visible_actualite', default
=False)
263 ancienid
= models
.IntegerField(
264 db_column
='ancienId_actualite', blank
=True, null
=True
266 source
= models
.ForeignKey(SourceActualite
, related_name
='actualites')
267 disciplines
= models
.ManyToManyField(
268 Discipline
, blank
=True, related_name
="actualites"
270 regions
= models
.ManyToManyField(
271 Region
, blank
=True, related_name
="actualites",
272 verbose_name
='régions'
275 objects
= ActualiteManager()
276 all_objects
= models
.Manager()
279 db_table
= u
'actualite'
282 def __unicode__(self
):
283 return "%s" % (self
.titre
)
285 def get_absolute_url(self
):
286 return reverse('actualite', kwargs
={'id': self
.id})
288 def assigner_disciplines(self
, disciplines
):
289 self
.disciplines
.add(*disciplines
)
291 def assigner_regions(self
, regions
):
292 self
.regions
.add(*regions
)
295 class ActualiteVoir(Actualite
):
299 verbose_name
= 'actualité (visualisation)'
300 verbose_name_plural
= 'actualités (visualisation)'
305 class EvenementQuerySet(SEPQuerySet
):
307 def filter_type(self
, type):
308 return self
.filter(type=type)
310 def filter_debut(self
, min=None, max=None):
311 return self
._filter_date('debut', min=min, max=max)
313 def filter_date_modification(self
, min=None, max=None):
314 return self
._filter_date('date_modification', min=min, max=max)
317 class EvenementSphinxQuerySet(SEPSphinxQuerySet
):
319 def __init__(self
, model
=None):
320 SEPSphinxQuerySet
.__init__(
321 self
, model
=model
, index
='savoirsenpartage_evenements',
322 weights
=dict(titre
=3)
325 def filter_type(self
, type):
326 return self
.add_to_query('@type "%s"' % type)
328 def filter_debut(self
, min=None, max=None):
329 return self
._filter_date('debut', min=min, max=max)
331 def filter_date_modification(self
, min=None, max=None):
332 return self
._filter_date('date_modification', min=min, max=max)
334 def filter_region(self
, region
):
335 return self
.add_to_query('@regions "%s"' % region
.nom
)
337 def filter_discipline(self
, discipline
):
338 return self
.add_to_query('@disciplines "%s"' % discipline
.nom
)
341 class EvenementManager(SEPManager
):
343 def get_query_set(self
):
344 return EvenementQuerySet(self
.model
).filter(approuve
=True)
346 def get_sphinx_query_set(self
):
347 return EvenementSphinxQuerySet(self
.model
).order_by('-debut')
349 def filter_type(self
, type):
350 return self
.get_query_set().filter_type(type)
352 def filter_debut(self
, min=None, max=None):
353 return self
.get_query_set().filter_debut(min=min, max=max)
355 def filter_date_modification(self
, min=None, max=None):
356 return self
.get_query_set().filter_date_modification(min=min, max=max)
359 def build_time_zone_choices(pays
=None):
360 timezones
= pytz
.country_timezones
[pays
] if pays
else pytz
.common_timezones
362 now
= datetime
.datetime
.now()
363 for tzname
in timezones
:
364 tz
= pytz
.timezone(tzname
)
365 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
367 offset
= tz
.utcoffset(now
)
368 except (AmbiguousTimeError
, NonExistentTimeError
):
369 # oups. On est en train de changer d'heure. Ça devrait être fini
371 offset
= tz
.utcoffset(now
+ datetime
.timedelta(days
=1))
372 seconds
= offset
.seconds
+ offset
.days
* 86400
373 (hours
, minutes
) = divmod(seconds
// 60, 60)
374 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) \
375 if minutes
else 'UTC%+d' % hours
376 result
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
378 return [(x
[1], x
[2]) for x
in result
]
381 class Evenement(models
.Model
):
382 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
383 (u
'Conférence', u
'Conférence'),
384 (u
'Appel à contribution', u
'Appel à contribution'),
385 (u
'Journée d\'étude', u
'Journée d\'étude'),
386 (u
'Autre', u
'Autre'))
387 TIME_ZONE_CHOICES
= build_time_zone_choices()
389 uid
= models
.CharField(max_length
=255, default
=str(uuid
.uuid1()))
390 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
391 titre
= models
.CharField(max_length
=255)
392 discipline
= models
.ForeignKey(
393 'Discipline', related_name
="discipline",
394 blank
=True, null
=True
396 discipline_secondaire
= models
.ForeignKey(
397 'Discipline', related_name
="discipline_secondaire",
398 verbose_name
=u
"discipline secondaire", blank
=True, null
=True
400 mots_cles
= models
.TextField('Mots-Clés', blank
=True, null
=True)
401 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
402 adresse
= models
.TextField()
403 ville
= models
.CharField(max_length
=100)
404 pays
= models
.ForeignKey(
405 Pays
, null
=True, related_name
='evenements', to_field
='code'
407 debut
= models
.DateTimeField(default
=datetime
.datetime
.now
)
408 fin
= models
.DateTimeField(default
=datetime
.datetime
.now
)
409 fuseau
= models
.CharField(
410 max_length
=100, choices
=TIME_ZONE_CHOICES
,
411 verbose_name
='fuseau horaire'
413 description
= models
.TextField()
414 contact
= models
.TextField(null
=True) # champ obsolète
415 prenom
= models
.CharField('prénom', max_length
=100)
416 nom
= models
.CharField(max_length
=100)
417 courriel
= models
.EmailField()
418 url
= models
.CharField(max_length
=255, blank
=True, null
=True)
419 piece_jointe
= models
.FileField(
420 upload_to
='agenda/pj', blank
=True, verbose_name
='pièce jointe'
422 regions
= models
.ManyToManyField(
423 Region
, blank
=True, related_name
="evenements",
424 verbose_name
='régions additionnelles',
425 help_text
="On considère d'emblée que l'évènement se déroule dans la "
426 "région dans laquelle se trouve le pays indiqué plus haut. Il est "
427 "possible de désigner ici des régions additionnelles."
429 date_modification
= models
.DateTimeField(
430 editable
=False, auto_now
=True, null
=True
433 objects
= EvenementManager()
434 all_objects
= models
.Manager()
437 ordering
= ['-debut']
438 verbose_name
= u
'évènement'
439 verbose_name_plural
= u
'évènements'
441 def __unicode__(self
):
442 return "[%s] %s" % (self
.uid
, self
.titre
)
444 def get_absolute_url(self
):
445 return reverse('evenement', kwargs
={'id': self
.id})
447 def duration_display(self
):
448 delta
= self
.fin
- self
.debut
449 minutes
, seconds
= divmod(delta
.seconds
, 60)
450 hours
, minutes
= divmod(minutes
, 60)
454 parts
.append('1 jour')
456 parts
.append('%d jours' % days
)
458 parts
.append('1 heure')
460 parts
.append('%d heures' % hours
)
462 parts
.append('1 minute')
464 parts
.append('%d minutes' % minutes
)
465 return ' '.join(parts
)
467 def piece_jointe_display(self
):
468 return self
.piece_jointe
and os
.path
.basename(self
.piece_jointe
.name
)
470 def courriel_display(self
):
471 return self
.courriel
.replace(u
'@', u
' (à) ')
477 bits
.append(self
.adresse
)
479 bits
.append(self
.ville
)
481 bits
.append(self
.pays
.nom
)
482 return ', '.join(bits
)
485 from django
.core
.exceptions
import ValidationError
486 if self
.debut
> self
.fin
:
487 raise ValidationError(
488 'La date de fin ne doit pas être antérieure à la date de début'
491 def save(self
, *args
, **kwargs
):
493 Sauvegarde l'objet dans django et le synchronise avec caldav s'il a
496 self
.contact
= '' # Vider ce champ obsolète à la première occasion...
498 super(Evenement
, self
).save(*args
, **kwargs
)
501 # methodes de commnunications avec CALDAV
503 """Retourne l'evenement django sous forme d'objet icalendar"""
504 cal
= vobject
.iCalendar()
507 # fournit son propre uid
508 if self
.uid
in [None, ""]:
509 self
.uid
= str(uuid
.uuid1())
511 cal
.vevent
.add('uid').value
= self
.uid
513 cal
.vevent
.add('summary').value
= self
.titre
515 if self
.mots_cles
is None:
518 kw
= self
.mots_cles
.split(",")
521 kw
.append(self
.discipline
.nom
)
522 kw
.append(self
.discipline_secondaire
.nom
)
527 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
529 cal
.vevent
.add('x-auf-keywords').value
= k
531 description
= self
.description
533 if len(self
.description
) > 0:
535 description
+= u
"Mots-clés: " + ", ".join(kw
)
537 cal
.vevent
.add('dtstart').value
= \
538 combine(self
.debut
, pytz
.timezone(self
.fuseau
))
539 cal
.vevent
.add('dtend').value
= \
540 combine(self
.fin
, pytz
.timezone(self
.fuseau
))
541 cal
.vevent
.add('created').value
= \
542 combine(datetime
.datetime
.now(), "UTC")
543 cal
.vevent
.add('dtstamp').value
= \
544 combine(datetime
.datetime
.now(), "UTC")
545 if len(description
) > 0:
546 cal
.vevent
.add('description').value
= description
547 if len(self
.contact
) > 0:
548 cal
.vevent
.add('contact').value
= self
.contact
549 if len(self
.url
) > 0:
550 cal
.vevent
.add('url').value
= self
.url
551 cal
.vevent
.add('location').value
= ', '.join(
552 x
for x
in [self
.adresse
, self
.ville
, self
.pays
.nom
] if x
554 if self
.piece_jointe
:
555 url
= self
.piece_jointe
.url
556 if not url
.startswith('http://'):
557 url
= SITE_ROOT_URL
+ url
558 cal
.vevent
.add('attach').value
= url
561 def update_vevent(self
,):
562 """Essaie de créer l'évènement sur le serveur ical.
563 En cas de succès, l'évènement local devient donc inactif et approuvé"""
566 event
= self
.as_ical()
567 client
= caldav
.DAVClient(CALENDRIER_URL
)
568 cal
= caldav
.Calendar(client
, url
=CALENDRIER_URL
)
570 client
, parent
=cal
, data
=event
.serialize(), id=self
.uid
574 self
.approuve
= False
576 def delete_vevent(self
,):
577 """Supprime l'evenement sur le serveur caldav"""
580 client
= caldav
.DAVClient(CALENDRIER_URL
)
581 cal
= caldav
.Calendar(client
, url
=CALENDRIER_URL
)
582 e
= cal
.event(self
.uid
)
584 except error
.NotFoundError
:
587 def assigner_regions(self
, regions
):
588 self
.regions
.add(*regions
)
590 def assigner_disciplines(self
, disciplines
):
591 if len(disciplines
) == 1:
593 self
.discipline_secondaire
= disciplines
[0]
595 self
.discipline
= disciplines
[0]
596 elif len(disciplines
) >= 2:
597 self
.discipline
= disciplines
[0]
598 self
.discipline_secondaire
= disciplines
[1]
601 def delete_vevent(sender
, instance
, *args
, **kwargs
):
602 # Surcharge du comportement de suppression
603 # La méthode de connexion par signals est préférable à surcharger la
604 # méthode delete() car dans le cas de la suppression par lots, cell-ci
606 instance
.delete_vevent()
607 pre_delete
.connect(delete_vevent
, sender
=Evenement
)
610 class EvenementVoir(Evenement
):
614 verbose_name
= 'événement (visualisation)'
615 verbose_name_plural
= 'événement (visualisation)'
620 class ListSet(models
.Model
):
621 spec
= models
.CharField(primary_key
=True, max_length
=255)
622 name
= models
.CharField(max_length
=255)
623 server
= models
.CharField(max_length
=255)
624 validated
= models
.BooleanField(default
=True)
626 def __unicode__(self
,):
630 class RecordCategorie(models
.Model
):
631 nom
= models
.CharField(max_length
=255)
634 verbose_name
= 'catégorie ressource'
635 verbose_name_plural
= 'catégories ressources'
637 def __unicode__(self
):
641 class RecordQuerySet(SEPQuerySet
):
643 def filter_modified(self
, min=None, max=None):
644 return self
._filter_date('modified', min=min, max=max)
647 class RecordSphinxQuerySet(SEPSphinxQuerySet
):
649 def __init__(self
, model
=None):
650 SEPSphinxQuerySet
.__init__(
651 self
, model
=model
, index
='savoirsenpartage_ressources',
652 weights
=dict(title
=3)
655 def filter_modified(self
, min=None, max=None):
656 return self
._filter_date('modified', min=min, max=max)
658 def filter_region(self
, region
):
659 return self
.filter(region_ids
=region
.id)
661 def filter_discipline(self
, discipline
):
662 return self
.filter(discipline_ids
=discipline
.id)
665 class RecordManager(SEPManager
):
667 def get_query_set(self
):
668 """Ne garder que les ressources validées et qui sont soit dans aucun
669 listset ou au moins dans un listset validé."""
670 ids_valides
= ListSet
.objects
.filter(validated
=True) \
671 .values('record__id')
672 ids_invalides
= ListSet
.objects
.filter(validated
=False) \
673 .values('record__id')
674 qs
= RecordQuerySet(self
.model
)
675 qs
= qs
.filter(validated
=True)
676 qs
= qs
.filter(Q(id__in
=ids_valides
) | ~
Q(id__in
=ids_invalides
))
679 def get_sphinx_query_set(self
):
680 return RecordSphinxQuerySet(self
.model
)
682 def filter_modified(self
, min=None, max=None):
683 return self
.get_query_set().filter_modified(min=min, max=max)
686 class Record(models
.Model
):
688 #fonctionnement interne
689 id = models
.AutoField(primary_key
=True)
690 server
= models
.CharField(max_length
=255, verbose_name
=u
'serveur')
691 last_update
= models
.CharField(max_length
=255)
692 last_checksum
= models
.CharField(max_length
=255)
693 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
696 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
697 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
698 description
= models
.TextField(null
=True, blank
=True)
699 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
700 identifier
= models
.CharField(
701 max_length
=255, null
=True, blank
=True, unique
=True
703 uri
= models
.CharField(max_length
=255, null
=True, blank
=True, unique
=True)
704 source
= models
.TextField(null
=True, blank
=True)
705 contributor
= models
.TextField(null
=True, blank
=True)
706 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
707 publisher
= models
.TextField(null
=True, blank
=True)
708 type = models
.TextField(null
=True, blank
=True)
709 format
= models
.TextField(null
=True, blank
=True)
710 language
= models
.TextField(null
=True, blank
=True)
712 listsets
= models
.ManyToManyField(ListSet
, null
=True, blank
=True)
714 #SEP 2 (aucune données récoltées)
715 alt_title
= models
.TextField(null
=True, blank
=True)
716 abstract
= models
.TextField(null
=True, blank
=True)
717 creation
= models
.CharField(max_length
=255, null
=True, blank
=True)
718 issued
= models
.CharField(max_length
=255, null
=True, blank
=True)
719 isbn
= models
.TextField(null
=True, blank
=True)
720 orig_lang
= models
.TextField(null
=True, blank
=True)
722 categorie
= models
.ForeignKey(
723 RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie'
726 # Metadata AUF multivaluées
727 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
728 thematiques
= models
.ManyToManyField(
729 Thematique
, blank
=True, verbose_name
='thématiques'
731 pays
= models
.ManyToManyField(Pays
, blank
=True)
732 regions
= models
.ManyToManyField(
733 Region
, blank
=True, verbose_name
='régions'
737 objects
= RecordManager()
738 all_objects
= models
.Manager()
741 verbose_name
= 'ressource'
743 def __unicode__(self
):
744 return "[%s] %s" % (self
.server
, self
.title
)
746 def get_absolute_url(self
):
747 return reverse('ressource', kwargs
={'id': self
.id})
749 def getServeurURL(self
):
750 """Retourne l'URL du serveur de provenance"""
751 return RESOURCES
[self
.server
]['url'] \
752 if self
.server
in RESOURCES
else ''
754 def est_complet(self
):
755 """teste si le record à toutes les données obligatoires"""
756 return self
.disciplines
.count() > 0 and \
757 self
.thematiques
.count() > 0 and \
758 self
.pays
.count() > 0 and \
759 self
.regions
.count() > 0
761 def assigner_regions(self
, regions
):
762 self
.regions
.add(*regions
)
764 def assigner_disciplines(self
, disciplines
):
765 self
.disciplines
.add(*disciplines
)
768 class RecordEdit(Record
):
772 verbose_name
= 'ressource (édition)'
773 verbose_name_plural
= 'ressources (édition)'
776 class Serveur(models
.Model
):
777 """Identification d'un serveur d'ou proviennent les références"""
778 nom
= models
.CharField(primary_key
=True, max_length
=255)
780 def __unicode__(self
,):
783 def conf_2_db(self
,):
784 for k
in RESOURCES
.keys():
785 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
790 class Profile(models
.Model
):
791 user
= models
.ForeignKey(User
, unique
=True)
792 serveurs
= models
.ManyToManyField(Serveur
, null
=True, blank
=True)
795 class HarvestLog(models
.Model
):
796 context
= models
.CharField(max_length
=255)
797 name
= models
.CharField(max_length
=255)
798 date
= models
.DateTimeField(auto_now
=True)
799 added
= models
.IntegerField(null
=True, blank
=True)
800 updated
= models
.IntegerField(null
=True, blank
=True)
801 processed
= models
.IntegerField(null
=True, blank
=True)
802 record
= models
.ForeignKey(Record
, null
=True, blank
=True)
806 logger
= HarvestLog()
807 if 'record_id' in message
:
808 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
809 del(message
['record_id'])
811 for k
, v
in message
.items():
812 setattr(logger
, k
, v
)
818 class PageStatique(models
.Model
):
819 id = models
.CharField(max_length
=32, primary_key
=True)
820 titre
= models
.CharField(max_length
=100)
821 contenu
= models
.TextField()
824 verbose_name_plural
= 'pages statiques'
829 class GlobalSearchResults(object):
831 def __init__(self
, actualites
=None, appels
=None, evenements
=None,
832 ressources
=None, chercheurs
=None, groupes
=None,
833 sites
=None, sites_auf
=None):
834 self
.actualites
= actualites
836 self
.evenements
= evenements
837 self
.ressources
= ressources
838 self
.chercheurs
= chercheurs
839 self
.groupes
= groupes
841 self
.sites_auf
= sites_auf
843 def __nonzero__(self
):
844 return bool(self
.actualites
or self
.appels
or self
.evenements
or
845 self
.ressources
or self
.chercheurs
or self
.groupes
or
846 self
.sites
or self
.sites_auf
)
849 class Search(models
.Model
):
850 user
= models
.ForeignKey(User
, editable
=False)
851 content_type
= models
.ForeignKey(ContentType
, editable
=False)
852 nom
= models
.CharField(max_length
=100, verbose_name
="nom de la recherche")
853 alerte_courriel
= models
.BooleanField(
854 verbose_name
="Envoyer une alerte courriel"
856 derniere_alerte
= models
.DateField(
857 verbose_name
="Date d'envoi de la dernière alerte courriel",
858 null
=True, editable
=False
860 q
= models
.CharField(
861 max_length
=255, blank
=True, verbose_name
="dans tous les champs"
863 discipline
= models
.ForeignKey(Discipline
, blank
=True, null
=True)
864 region
= models
.ForeignKey(
865 Region
, blank
=True, null
=True, verbose_name
='région',
866 help_text
="La région est ici définie au sens, non strictement "
867 "géographique, du Bureau régional de l'AUF de référence."
870 def query_string(self
):
872 for field
in self
._meta
.fields
:
873 if field
.name
in ['id', 'user', 'nom', 'search_ptr',
876 value
= getattr(self
, field
.column
)
878 if isinstance(value
, datetime
.date
):
879 params
[field
.name
] = value
.strftime('%d/%m/%Y')
881 params
[field
.name
] = smart_str(value
)
882 return urlencode(params
)
885 verbose_name
= 'recherche transversale'
886 verbose_name_plural
= "recherches transversales"
888 def __unicode__(self
):
892 if self
.alerte_courriel
:
894 original_search
= Search
.objects
.get(id=self
.id)
895 if not original_search
.alerte_courriel
:
896 # On a nouvellement activé l'alerte courriel. Notons la
898 self
.derniere_alerte
= \
899 datetime
.date
.today() - datetime
.timedelta(days
=1)
900 except Search
.DoesNotExist
:
901 self
.derniere_alerte
= \
902 datetime
.date
.today() - datetime
.timedelta(days
=1)
903 if (not self
.content_type_id
):
904 self
.content_type
= ContentType
.objects
.get_for_model(
907 super(Search
, self
).save()
909 def as_leaf_class(self
):
910 content_type
= self
.content_type
911 model
= content_type
.model_class()
914 return model
.objects
.get(id=self
.id)
916 def run(self
, min_date
=None, max_date
=None):
917 from chercheurs
.models
import Chercheur
, Groupe
918 from sitotheque
.models
import Site
920 actualites
= Actualite
.objects
921 evenements
= Evenement
.objects
922 ressources
= Record
.objects
923 chercheurs
= Chercheur
.objects
924 groupes
= Groupe
.objects
927 actualites
= actualites
.search(self
.q
)
928 evenements
= evenements
.search(self
.q
)
929 ressources
= ressources
.search(self
.q
)
930 chercheurs
= chercheurs
.search(self
.q
)
931 groupes
= groupes
.search(self
.q
)
932 sites
= sites
.search(self
.q
)
934 actualites
= actualites
.filter_discipline(self
.discipline
)
935 evenements
= evenements
.filter_discipline(self
.discipline
)
936 ressources
= ressources
.filter_discipline(self
.discipline
)
937 chercheurs
= chercheurs
.filter_discipline(self
.discipline
)
938 sites
= sites
.filter_discipline(self
.discipline
)
940 actualites
= actualites
.filter_region(self
.region
)
941 evenements
= evenements
.filter_region(self
.region
)
942 ressources
= ressources
.filter_region(self
.region
)
943 chercheurs
= chercheurs
.filter_region(self
.region
)
944 sites
= sites
.filter_region(self
.region
)
946 actualites
= actualites
.filter_date(min=min_date
)
947 evenements
= evenements
.filter_date_modification(min=min_date
)
948 ressources
= ressources
.filter_modified(min=min_date
)
949 chercheurs
= chercheurs
.filter_date_modification(min=min_date
)
950 sites
= sites
.filter_date_maj(min=min_date
)
952 actualites
= actualites
.filter_date(max=max_date
)
953 evenements
= evenements
.filter_date_modification(max=max_date
)
954 ressources
= ressources
.filter_modified(max=max_date
)
955 chercheurs
= chercheurs
.filter_date_modification(max=max_date
)
956 sites
= sites
.filter_date_maj(max=max_date
)
959 sites_auf
= google_search(0, self
.q
)['results']
963 return GlobalSearchResults(
964 actualites
=actualites
.order_by('-date').filter_type('actu'),
965 appels
=actualites
.order_by('-date').filter_type('appels'),
966 evenements
=evenements
.order_by('-debut'),
967 ressources
=ressources
.order_by('-modified'),
968 chercheurs
=chercheurs
.order_by('-date_modification'),
969 groupes
=groupes
.order_by('nom'),
970 sites
=sites
.order_by('-date_maj'),
976 if self
.content_type
.model
!= 'search':
977 obj
= self
.content_type
.get_object_for_this_type(pk
=self
.pk
)
982 url
+= '/discipline/%d' % self
.discipline
.id
984 url
+= '/region/%d' % self
.region
.id
987 url
+= '?' + urlencode({'q': smart_str(self
.q
)})
993 def send_email_alert(self
):
994 """Envoie une alerte courriel correspondant à cette recherche"""
995 yesterday
= datetime
.date
.today() - datetime
.timedelta(days
=1)
996 if self
.derniere_alerte
is not None:
997 results
= self
.as_leaf_class().run(
998 min_date
=self
.derniere_alerte
, max_date
=yesterday
1001 subject
= 'Savoirs en partage - ' + self
.nom
1002 from_email
= CONTACT_EMAIL
1003 to_email
= self
.user
.email
1004 text_content
= u
'Voici les derniers résultats ' \
1005 u
'correspondant à votre recherche sauvegardée.\n\n'
1006 text_content
+= self
.as_leaf_class() \
1007 .get_email_alert_content(results
)
1008 text_content
+= u
'''
1010 Pour modifier votre abonnement aux alertes courriel de Savoirs en partage,
1011 rendez-vous sur le [gestionnaire de recherches sauvegardées](%s%s)''' % (
1012 SITE_ROOT_URL
, reverse('recherches')
1015 '<div style="font-family: Arial, sans-serif">\n' + \
1016 markdown(smart_str(text_content
)) + '</div>\n'
1017 msg
= EmailMultiAlternatives(
1018 subject
, text_content
, from_email
, [to_email
]
1020 msg
.attach_alternative(html_content
, "text/html")
1022 self
.derniere_alerte
= yesterday
1025 def get_email_alert_content(self
, results
):
1027 if results
.chercheurs
:
1028 content
+= u
'\n### Nouveaux chercheurs\n\n'
1029 for chercheur
in results
.chercheurs
:
1030 content
+= u
'- [%s %s](%s%s) \n' % (
1031 chercheur
.nom
.upper(), chercheur
.prenom
, SITE_ROOT_URL
,
1032 chercheur
.get_absolute_url()
1034 content
+= u
' %s\n\n' % chercheur
.etablissement_display
1035 if results
.ressources
:
1036 content
+= u
'\n### Nouvelles ressources\n\n'
1037 for ressource
in results
.ressources
:
1038 content
+= u
'- [%s](%s%s)\n\n' % (
1039 ressource
.title
, SITE_ROOT_URL
,
1040 ressource
.get_absolute_url()
1042 if ressource
.description
:
1046 for line
in textwrap
.wrap(ressource
.description
)
1050 if results
.actualites
:
1051 content
+= u
'\n### Nouvelles actualités\n\n'
1052 for actualite
in results
.actualites
:
1053 content
+= u
'- [%s](%s%s)\n\n' % (
1054 actualite
.titre
, SITE_ROOT_URL
,
1055 actualite
.get_absolute_url()
1061 for line
in textwrap
.wrap(actualite
.texte
)
1065 content
+= u
"\n### Nouveaux appels d'offres\n\n"
1066 for appel
in results
.appels
:
1067 content
+= u
'- [%s](%s%s)\n\n' % (appel
.titre
,
1069 appel
.get_absolute_url())
1074 for line
in textwrap
.wrap(appel
.texte
)
1077 if results
.evenements
:
1078 content
+= u
"\n### Nouveaux évènements\n\n"
1079 for evenement
in results
.evenements
:
1080 content
+= u
'- [%s](%s%s) \n' % (
1081 evenement
.titre
, SITE_ROOT_URL
,
1082 evenement
.get_absolute_url()
1084 content
+= u
' où ? : %s \n' % evenement
.lieu
1085 content
+= evenement
.debut
.strftime(
1086 ' quand ? : %d/%m/%Y %H:%M \n'
1088 content
+= u
' durée ? : %s\n\n' % \
1089 evenement
.duration_display()
1090 content
+= u
' quoi ? : '
1091 content
+= '\n '.join(
1092 textwrap
.wrap(evenement
.description
)
1096 content
+= u
"\n### Nouveaux sites\n\n"
1097 for site
in results
.sites
:
1098 content
+= u
'- [%s](%s%s)\n\n' % (site
.titre
,
1100 site
.get_absolute_url())
1101 if site
.description
:
1105 for line
in textwrap
.wrap(site
.description
)
1111 class RessourceSearch(Search
):
1112 auteur
= models
.CharField(
1113 max_length
=100, blank
=True, verbose_name
="auteur ou contributeur"
1115 titre
= models
.CharField(max_length
=100, blank
=True)
1116 sujet
= models
.CharField(max_length
=100, blank
=True)
1117 publisher
= models
.CharField(
1118 max_length
=100, blank
=True, verbose_name
="éditeur"
1120 categorie
= models
.ForeignKey(
1121 RecordCategorie
, blank
=True, null
=True, verbose_name
='catégorie'
1125 verbose_name
= 'recherche de ressources'
1126 verbose_name_plural
= "recherches de ressources"
1128 def run(self
, min_date
=None, max_date
=None):
1129 results
= Record
.objects
1131 results
= results
.search(self
.q
)
1133 results
= results
.add_to_query(
1134 '@(creator,contributor) ' + self
.auteur
1137 results
= results
.add_to_query('@title ' + self
.titre
)
1139 results
= results
.add_to_query('@subject ' + self
.sujet
)
1141 results
= results
.add_to_query('@publisher ' + self
.publisher
)
1143 results
= results
.add_to_query('@categorie %s' % self
.categorie
.id)
1145 results
= results
.filter_discipline(self
.discipline
)
1147 results
= results
.filter_region(self
.region
)
1149 results
= results
.filter_modified(min=min_date
)
1151 results
= results
.filter_modified(max=max_date
)
1153 """Montrer les résultats les plus récents si on n'a pas fait
1154 une recherche par mots-clés."""
1155 results
= results
.order_by('-modified')
1156 return results
.all()
1159 qs
= self
.query_string()
1160 return reverse('ressources') + ('?' + qs
if qs
else '')
1163 qs
= self
.query_string()
1164 return reverse('rss_ressources') + ('?' + qs
if qs
else '')
1166 def get_email_alert_content(self
, results
):
1168 for ressource
in results
:
1169 content
+= u
'- [%s](%s%s)\n\n' % (ressource
.title
,
1171 ressource
.get_absolute_url())
1172 if ressource
.description
:
1176 for line
in textwrap
.wrap(ressource
.description
)
1182 class ActualiteSearchBase(Search
):
1183 date_min
= models
.DateField(
1184 blank
=True, null
=True, verbose_name
="depuis le"
1186 date_max
= models
.DateField(
1187 blank
=True, null
=True, verbose_name
="jusqu'au"
1193 def run(self
, min_date
=None, max_date
=None):
1194 results
= Actualite
.objects
1196 results
= results
.search(self
.q
)
1198 results
= results
.filter_discipline(self
.discipline
)
1200 results
= results
.filter_region(self
.region
)
1202 results
= results
.filter_date(min=self
.date_min
)
1204 results
= results
.filter_date(max=self
.date_max
)
1206 results
= results
.filter_date(min=min_date
)
1208 results
= results
.filter_date(max=max_date
)
1209 return results
.all()
1211 def get_email_alert_content(self
, results
):
1213 for actualite
in results
:
1214 content
+= u
'- [%s](%s%s)\n\n' % (actualite
.titre
,
1216 actualite
.get_absolute_url())
1221 for line
in textwrap
.wrap(actualite
.texte
)
1227 class ActualiteSearch(ActualiteSearchBase
):
1230 verbose_name
= "recherche d'actualités"
1231 verbose_name_plural
= "recherches d'actualités"
1233 def run(self
, min_date
=None, max_date
=None):
1234 return super(ActualiteSearch
, self
) \
1235 .run(min_date
=min_date
, max_date
=max_date
) \
1236 .filter_type('actu')
1239 qs
= self
.query_string()
1240 return reverse('actualites') + ('?' + qs
if qs
else '')
1243 qs
= self
.query_string()
1244 return reverse('rss_actualites') + ('?' + qs
if qs
else '')
1247 class AppelSearch(ActualiteSearchBase
):
1250 verbose_name
= "recherche d'appels d'offres"
1251 verbose_name_plural
= "recherches d'appels d'offres"
1253 def run(self
, min_date
=None, max_date
=None):
1254 return super(AppelSearch
, self
) \
1255 .run(min_date
=min_date
, max_date
=max_date
) \
1256 .filter_type('appels')
1259 qs
= self
.query_string()
1260 return reverse('appels') + ('?' + qs
if qs
else '')
1263 qs
= self
.query_string()
1264 return reverse('rss_appels') + ('?' + qs
if qs
else '')
1267 class EvenementSearch(Search
):
1268 titre
= models
.CharField(
1269 max_length
=100, blank
=True, verbose_name
="Intitulé"
1271 type = models
.CharField(
1272 max_length
=100, blank
=True, choices
=Evenement
.TYPE_CHOICES
1274 date_min
= models
.DateField(
1275 blank
=True, null
=True, verbose_name
="depuis le"
1277 date_max
= models
.DateField(
1278 blank
=True, null
=True, verbose_name
="jusqu'au"
1282 verbose_name
= "recherche d'évènements"
1283 verbose_name_plural
= "recherches d'évènements"
1285 def run(self
, min_date
=None, max_date
=None):
1286 results
= Evenement
.objects
1288 results
= results
.search(self
.q
)
1290 results
= results
.add_to_query('@titre ' + self
.titre
)
1292 results
= results
.filter_discipline(self
.discipline
)
1294 results
= results
.filter_region(self
.region
)
1296 results
= results
.filter_type(self
.type)
1298 results
= results
.filter_debut(min=self
.date_min
)
1300 results
= results
.filter_debut(max=self
.date_max
)
1302 results
= results
.filter_date_modification(min=min_date
)
1304 results
= results
.filter_date_modification(max=max_date
)
1305 return results
.all()
1308 qs
= self
.query_string()
1309 return reverse('agenda') + ('?' + qs
if qs
else '')
1312 qs
= self
.query_string()
1313 return reverse('rss_agenda') + ('?' + qs
if qs
else '')
1315 def get_email_alert_content(self
, results
):
1317 for evenement
in results
:
1318 content
+= u
'- [%s](%s%s) \n' % (evenement
.titre
,
1320 evenement
.get_absolute_url())
1321 content
+= u
' où ? : %s \n' % evenement
.lieu
1322 content
+= evenement
.debut
.strftime(
1323 ' quand ? : %d/%m/%Y %H:%M \n'
1325 content
+= u
' durée ? : %s\n\n' % evenement
.duration_display()
1326 content
+= u
' quoi ? : '
1327 content
+= '\n '.join(
1328 textwrap
.wrap(evenement
.description
)