1 # -*- encoding: utf-8 -*-
11 from backend_config
import RESOURCES
12 from babel
.dates
import get_timezone_name
13 from caldav
.lib
import error
14 from babel
.dates
import get_timezone_name
15 from datamaster_modeles
.models
import Region
, Pays
, Thematique
16 from django
.contrib
.auth
.models
import User
17 from django
.db
import models
18 from django
.db
.models
import Q
, Max
19 from django
.db
.models
.signals
import pre_delete
20 from django
.utils
.encoding
import smart_unicode
21 from djangosphinx
.models
import SphinxQuerySet
22 from savoirs
.globals import META
23 from settings
import CALENDRIER_URL
, SITE_ROOT_URL
25 # Fonctionnalités communes à tous les query sets
27 class RandomQuerySetMixin(object):
28 """Mixin pour les modèles.
30 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
31 méthode pour récupérer des objets au hasard.
34 def random(self
, n
=1):
35 """Récupère aléatoirement un nombre donné d'objets."""
37 positions
= random
.sample(xrange(count
), min(n
, count
))
38 return [self
[p
] for p
in positions
]
40 class SEPQuerySet(models
.query
.QuerySet
, RandomQuerySetMixin
):
43 class SEPSphinxQuerySet(SphinxQuerySet
, RandomQuerySetMixin
):
44 """Fonctionnalités communes aux query sets de Sphinx."""
46 def __init__(self
, model
=None, index
=None, weights
=None):
47 SphinxQuerySet
.__init__(self
, model
=model
, index
=index
,
48 mode
='SPH_MATCH_EXTENDED2',
49 rankmode
='SPH_RANK_PROXIMITY_BM25',
52 def add_to_query(self
, query
):
53 """Ajoute une partie à la requête texte."""
55 # Assurons-nous qu'il y a un nombre pair de guillemets
56 if query
.count('"') % 2 != 0:
57 # Sinon, on enlève le dernier (faut choisir...)
59 query
= query
[:i
] + query
[i
+1:]
61 new_query
= smart_unicode(self
._query
) + ' ' + query
if self
._query
else query
62 return self
.query(new_query
)
64 def search(self
, text
):
65 """Recherche ``text`` dans tous les champs."""
66 return self
.add_to_query('@* ' + text
)
68 def filter_discipline(self
, discipline
):
69 """Par défaut, le filtre par discipline cherche le nom de la
70 discipline dans tous les champs."""
71 return self
.search('"%s"' % discipline
.nom
)
73 def filter_region(self
, region
):
74 """Par défaut, le filtre par région cherche le nom de la région dans
76 return self
.search('"%s"' % region
.nom
)
78 class SEPManager(models
.Manager
):
79 """Lorsque les méthodes ``search``, ``filter_region`` et
80 ``filter_discipline`` sont appelées sur ce manager, le query set
81 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
83 def query(self
, query
):
84 return self
.get_sphinx_query_set().query(query
)
86 def add_to_query(self
, query
):
87 return self
.get_sphinx_query_set().add_to_query(query
)
89 def search(self
, text
):
90 return self
.get_sphinx_query_set().search(text
)
92 def filter_region(self
, region
):
93 return self
.get_sphinx_query_set().filter_region(region
)
95 def filter_discipline(self
, discipline
):
96 return self
.get_sphinx_query_set().filter_discipline(discipline
)
100 class Discipline(models
.Model
):
101 id = models
.IntegerField(primary_key
=True, db_column
='id_discipline')
102 nom
= models
.CharField(max_length
=765, db_column
='nom_discipline')
104 def __unicode__ (self
):
108 db_table
= u
'discipline'
113 class SourceActualite(models
.Model
):
114 nom
= models
.CharField(max_length
=255)
115 url
= models
.CharField(max_length
=255, verbose_name
='URL')
118 verbose_name
= u
'fil RSS syndiqué'
119 verbose_name_plural
= u
'fils RSS syndiqués'
121 def __unicode__(self
,):
122 return u
"%s" % self
.nom
125 """Mise à jour du fil RSS."""
126 feed
= feedparser
.parse(self
.url
)
127 for entry
in feed
.entries
:
128 if Actualite
.all_objects
.filter(url
=entry
.link
).count() == 0:
129 ts
= entry
.updated_parsed
130 date
= datetime
.date(ts
.tm_year
, ts
.tm_mon
, ts
.tm_mday
)
131 a
= self
.actualites
.create(titre
=entry
.title
,
132 texte
=entry
.summary_detail
.value
,
133 url
=entry
.link
, date
=date
)
135 class ActualiteQuerySet(SEPQuerySet
):
137 def filter_date(self
, min=None, max=None):
140 qs
= qs
.filter(date__gte
=min)
142 qs
= qs
.filter(date__lte
=max)
145 class ActualiteSphinxQuerySet(SEPSphinxQuerySet
):
147 def __init__(self
, model
=None):
148 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_actualites',
149 weights
=dict(titre
=3))
151 class ActualiteManager(SEPManager
):
153 def get_query_set(self
):
154 return ActualiteQuerySet(self
.model
).filter(visible
=True)
156 def get_sphinx_query_set(self
):
157 return ActualiteSphinxQuerySet(self
.model
).order_by('-date')
159 def filter_date(self
, min=None, max=None):
160 return self
.get_query_set().filter_date(min=min, max=max)
162 class Actualite(models
.Model
):
163 id = models
.AutoField(primary_key
=True, db_column
='id_actualite')
164 titre
= models
.CharField(max_length
=765, db_column
='titre_actualite')
165 texte
= models
.TextField(db_column
='texte_actualite')
166 url
= models
.CharField(max_length
=765, db_column
='url_actualite')
167 date
= models
.DateField(db_column
='date_actualite')
168 visible
= models
.BooleanField(db_column
='visible_actualite', default
=False)
169 ancienid
= models
.IntegerField(db_column
='ancienId_actualite', blank
=True, null
=True)
170 source
= models
.ForeignKey(SourceActualite
, blank
=True, null
=True, related_name
='actualites')
171 disciplines
= models
.ManyToManyField(Discipline
, blank
=True, related_name
="actualites")
172 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="actualites", verbose_name
='régions')
174 objects
= ActualiteManager()
175 all_objects
= models
.Manager()
178 db_table
= u
'actualite'
181 def __unicode__ (self
):
182 return "%s" % (self
.titre
)
184 def assigner_disciplines(self
, disciplines
):
185 self
.disciplines
.add(*disciplines
)
187 def assigner_regions(self
, regions
):
188 self
.regions
.add(*regions
)
192 class EvenementQuerySet(SEPQuerySet
):
194 def filter_type(self
, type):
195 return self
.filter(type=type)
197 def filter_debut(self
, min=None, max=None):
200 qs
= qs
.filter(debut__gte
=min)
202 qs
= qs
.filter(debut__lt
=max+datetime
.timedelta(days
=1))
205 class EvenementSphinxQuerySet(SEPSphinxQuerySet
):
207 def __init__(self
, model
=None):
208 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_evenements',
209 weights
=dict(titre
=3))
211 def filter_type(self
, type):
212 return self
.add_to_query('@type "%s"' % type)
214 def filter_debut(self
, min=None, max=None):
217 qs
= qs
.filter(debut__gte
=min.toordinal()+365)
219 qs
= qs
.filter(debut__lte
=max.toordinal()+365)
222 class EvenementManager(SEPManager
):
224 def get_query_set(self
):
225 return EvenementQuerySet(self
.model
).filter(approuve
=True)
227 def get_sphinx_query_set(self
):
228 return EvenementSphinxQuerySet(self
.model
).order_by('-debut')
230 def filter_type(self
, type):
231 return self
.get_query_set().filter_type(type)
233 def filter_debut(self
, min=None, max=None):
234 return self
.get_query_set().filter_debut(min=min, max=max)
236 def build_time_zone_choices(pays
=None):
238 timezones
= pytz
.country_timezones
[pays
] if pays
else pytz
.common_timezones
240 now
= datetime
.datetime
.now()
241 for tzname
in timezones
:
242 tz
= pytz
.timezone(tzname
)
243 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
244 if fr_name
in fr_names
:
246 fr_names
.add(fr_name
)
247 offset
= tz
.utcoffset(now
)
248 seconds
= offset
.seconds
+ offset
.days
* 86400
249 (hours
, minutes
) = divmod(seconds
// 60, 60)
250 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) if minutes
else 'UTC%+d' % hours
251 result
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
253 return [(x
[1], x
[2]) for x
in result
]
255 class Evenement(models
.Model
):
256 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
257 (u
'Conférence', u
'Conférence'),
258 (u
'Appel à contribution', u
'Appel à contribution'),
259 (u
'Journée d\'étude', u
'Journée d\'étude'),
261 TIME_ZONE_CHOICES
= build_time_zone_choices()
263 uid
= models
.CharField(max_length
=255, default
=str(uuid
.uuid1()))
264 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
265 titre
= models
.CharField(max_length
=255)
266 discipline
= models
.ForeignKey('Discipline', related_name
= "discipline",
267 blank
= True, null
= True)
268 discipline_secondaire
= models
.ForeignKey('Discipline', related_name
="discipline_secondaire",
269 verbose_name
=u
"discipline secondaire",
270 blank
=True, null
=True)
271 mots_cles
= models
.TextField('Mots-Clés', blank
=True, null
=True)
272 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
273 lieu
= models
.TextField()
274 debut
= models
.DateTimeField(default
=datetime
.datetime
.now
)
275 fin
= models
.DateTimeField(default
=datetime
.datetime
.now
)
276 pays
= models
.ForeignKey(Pays
, related_name
='evenements', null
=True, blank
=True)
277 fuseau
= models
.CharField(max_length
=100, choices
=TIME_ZONE_CHOICES
, verbose_name
='fuseau horaire')
278 description
= models
.TextField(blank
=True, null
=True)
279 contact
= models
.TextField(blank
=True, null
=True)
280 url
= models
.CharField(max_length
=255, blank
=True, null
=True)
281 piece_jointe
= models
.FileField(upload_to
='agenda/pj', blank
=True, verbose_name
='pièce jointe')
282 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="evenements", verbose_name
='régions')
284 objects
= EvenementManager()
285 all_objects
= models
.Manager()
288 ordering
= ['-debut']
290 def __unicode__(self
,):
291 return "[%s] %s" % (self
.uid
, self
.titre
)
293 def duration_display(self
):
294 delta
= self
.fin
- self
.debut
295 minutes
, seconds
= divmod(delta
.seconds
, 60)
296 hours
, minutes
= divmod(minutes
, 60)
300 parts
.append('1 jour')
302 parts
.append('%d jours' % days
)
304 parts
.append('1 heure')
306 parts
.append('%d heures' % hours
)
308 parts
.append('1 minute')
310 parts
.append('%d minutes' % minutes
)
311 return ' '.join(parts
)
313 def piece_jointe_display(self
):
314 return self
.piece_jointe
and os
.path
.basename(self
.piece_jointe
.name
)
317 from django
.core
.exceptions
import ValidationError
318 if self
.debut
> self
.fin
:
319 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
321 def save(self
, *args
, **kwargs
):
322 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
325 super(Evenement
, self
).save(*args
, **kwargs
)
328 # methodes de commnunications avec CALDAV
330 """Retourne l'evenement django sous forme d'objet icalendar"""
331 cal
= vobject
.iCalendar()
334 # fournit son propre uid
335 if self
.uid
in [None, ""]:
336 self
.uid
= str(uuid
.uuid1())
338 cal
.vevent
.add('uid').value
= self
.uid
340 cal
.vevent
.add('summary').value
= self
.titre
342 if self
.mots_cles
is None:
345 kw
= self
.mots_cles
.split(",")
348 kw
.append(self
.discipline
.nom
)
349 kw
.append(self
.discipline_secondaire
.nom
)
353 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
355 cal
.vevent
.add('x-auf-keywords').value
= k
357 description
= self
.description
359 if len(self
.description
) > 0:
361 description
+= u
"Mots-clés: " + ", ".join(kw
)
363 cal
.vevent
.add('dtstart').value
= combine(self
.debut
, pytz
.timezone(self
.fuseau
))
364 cal
.vevent
.add('dtend').value
= combine(self
.fin
, pytz
.timezone(self
.fuseau
))
365 cal
.vevent
.add('created').value
= combine(datetime
.datetime
.now(), "UTC")
366 cal
.vevent
.add('dtstamp').value
= combine(datetime
.datetime
.now(), "UTC")
367 if len(description
) > 0:
368 cal
.vevent
.add('description').value
= description
369 if len(self
.contact
) > 0:
370 cal
.vevent
.add('contact').value
= self
.contact
371 if len(self
.url
) > 0:
372 cal
.vevent
.add('url').value
= self
.url
373 if len(self
.lieu
) > 0:
374 cal
.vevent
.add('location').value
= self
.lieu
375 if self
.piece_jointe
:
376 url
= self
.piece_jointe
.url
377 if not url
.startswith('http://'):
378 url
= SITE_ROOT_URL
+ url
379 cal
.vevent
.add('attach').value
= url
382 def update_vevent(self
,):
383 """Essaie de créer l'évènement sur le serveur ical.
384 En cas de succès, l'évènement local devient donc inactif et approuvé"""
387 event
= self
.as_ical()
388 client
= caldav
.DAVClient(CALENDRIER_URL
)
389 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
390 e
= caldav
.Event(client
, parent
= cal
, data
= event
.serialize(), id=self
.uid
)
393 self
.approuve
= False
395 def delete_vevent(self
,):
396 """Supprime l'evenement sur le serveur caldav"""
399 event
= self
.as_ical()
400 client
= caldav
.DAVClient(CALENDRIER_URL
)
401 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
402 e
= cal
.event(self
.uid
)
404 except error
.NotFoundError
:
407 def assigner_regions(self
, regions
):
408 self
.regions
.add(*regions
)
410 def assigner_disciplines(self
, disciplines
):
411 if len(disciplines
) == 1:
413 self
.discipline_secondaire
= disciplines
[0]
415 self
.discipline
= disciplines
[0]
416 elif len(disciplines
) >= 2:
417 self
.discipline
= disciplines
[0]
418 self
.discipline_secondaire
= disciplines
[1]
420 def delete_vevent(sender
, instance
, *args
, **kwargs
):
421 # Surcharge du comportement de suppression
422 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
423 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
424 instance
.delete_vevent()
425 pre_delete
.connect(delete_vevent
, sender
=Evenement
)
429 class ListSet(models
.Model
):
430 spec
= models
.CharField(primary_key
= True, max_length
= 255)
431 name
= models
.CharField(max_length
= 255)
432 server
= models
.CharField(max_length
= 255)
433 validated
= models
.BooleanField(default
= True)
435 def __unicode__(self
,):
438 class RecordSphinxQuerySet(SEPSphinxQuerySet
):
440 def __init__(self
, model
=None):
441 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_ressources',
442 weights
=dict(title
=3))
444 class RecordManager(SEPManager
):
446 def get_query_set(self
):
447 """Ne garder que les ressources validées et qui sont soit dans aucun
448 listset ou au moins dans un listset validé."""
449 qs
= SEPQuerySet(self
.model
)
450 qs
= qs
.filter(validated
=True)
451 qs
= qs
.filter(Q(listsets__isnull
=True) |
Q(listsets__validated
=True))
454 def get_sphinx_query_set(self
):
455 return RecordSphinxQuerySet(self
.model
)
457 class Record(models
.Model
):
459 #fonctionnement interne
460 id = models
.AutoField(primary_key
= True)
461 server
= models
.CharField(max_length
= 255, verbose_name
=u
'serveur')
462 last_update
= models
.CharField(max_length
= 255)
463 last_checksum
= models
.CharField(max_length
= 255)
464 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
467 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
468 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
469 description
= models
.TextField(null
=True, blank
=True)
470 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
471 identifier
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
472 uri
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
473 source
= models
.TextField(null
= True, blank
= True)
474 contributor
= models
.TextField(null
= True, blank
= True)
475 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
476 publisher
= models
.TextField(null
= True, blank
= True)
477 type = models
.TextField(null
= True, blank
= True)
478 format
= models
.TextField(null
= True, blank
= True)
479 language
= models
.TextField(null
= True, blank
= True)
481 listsets
= models
.ManyToManyField(ListSet
, null
= True, blank
= True)
483 #SEP 2 (aucune données récoltées)
484 alt_title
= models
.TextField(null
= True, blank
= True)
485 abstract
= models
.TextField(null
= True, blank
= True)
486 creation
= models
.CharField(max_length
= 255, null
= True, blank
= True)
487 issued
= models
.CharField(max_length
= 255, null
= True, blank
= True)
488 isbn
= models
.TextField(null
= True, blank
= True)
489 orig_lang
= models
.TextField(null
= True, blank
= True)
491 # Metadata AUF multivaluées
492 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
493 thematiques
= models
.ManyToManyField(Thematique
, blank
=True, verbose_name
='thématiques')
494 pays
= models
.ManyToManyField(Pays
, blank
=True)
495 regions
= models
.ManyToManyField(Region
, blank
=True, verbose_name
='régions')
498 objects
= RecordManager()
499 all_objects
= models
.Manager()
502 verbose_name
= 'ressource'
504 def __unicode__(self
):
505 return "[%s] %s" % (self
.server
, self
.title
)
507 def getServeurURL(self
):
508 """Retourne l'URL du serveur de provenance"""
509 return RESOURCES
[self
.server
]['url']
511 def est_complet(self
):
512 """teste si le record à toutes les données obligatoires"""
513 return self
.disciplines
.count() > 0 and \
514 self
.thematiques
.count() > 0 and \
515 self
.pays
.count() > 0 and \
516 self
.regions
.count() > 0
518 def assigner_regions(self
, regions
):
519 self
.regions
.add(*regions
)
521 def assigner_disciplines(self
, disciplines
):
522 self
.disciplines
.add(*disciplines
)
524 class Serveur(models
.Model
):
525 """Identification d'un serveur d'ou proviennent les références"""
526 nom
= models
.CharField(primary_key
= True, max_length
= 255)
528 def __unicode__(self
,):
531 def conf_2_db(self
,):
532 for k
in RESOURCES
.keys():
533 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
537 class Profile(models
.Model
):
538 user
= models
.ForeignKey(User
, unique
=True)
539 serveurs
= models
.ManyToManyField(Serveur
, null
= True, blank
= True)
541 class HarvestLog(models
.Model
):
542 context
= models
.CharField(max_length
= 255)
543 name
= models
.CharField(max_length
= 255)
544 date
= models
.DateTimeField(auto_now
= True)
545 added
= models
.IntegerField(null
= True, blank
= True)
546 updated
= models
.IntegerField(null
= True, blank
= True)
547 processed
= models
.IntegerField(null
= True, blank
= True)
548 record
= models
.ForeignKey(Record
, null
= True, blank
= True)
552 logger
= HarvestLog()
553 if message
.has_key('record_id'):
554 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
555 del(message
['record_id'])
557 for k
,v
in message
.items():
558 setattr(logger
, k
, v
)