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):
237 timezones
= pytz
.country_timezones
[pays
] if pays
else pytz
.common_timezones
239 now
= datetime
.datetime
.now()
240 for tzname
in timezones
:
241 tz
= pytz
.timezone(tzname
)
242 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
243 offset
= tz
.utcoffset(now
)
244 seconds
= offset
.seconds
+ offset
.days
* 86400
245 (hours
, minutes
) = divmod(seconds
// 60, 60)
246 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) if minutes
else 'UTC%+d' % hours
247 result
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
249 return [(x
[1], x
[2]) for x
in result
]
251 class Evenement(models
.Model
):
252 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
253 (u
'Conférence', u
'Conférence'),
254 (u
'Appel à contribution', u
'Appel à contribution'),
255 (u
'Journée d\'étude', u
'Journée d\'étude'),
257 TIME_ZONE_CHOICES
= build_time_zone_choices()
259 uid
= models
.CharField(max_length
=255, default
=str(uuid
.uuid1()))
260 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
261 titre
= models
.CharField(max_length
=255)
262 discipline
= models
.ForeignKey('Discipline', related_name
= "discipline",
263 blank
= True, null
= True)
264 discipline_secondaire
= models
.ForeignKey('Discipline', related_name
="discipline_secondaire",
265 verbose_name
=u
"discipline secondaire",
266 blank
=True, null
=True)
267 mots_cles
= models
.TextField('Mots-Clés', blank
=True, null
=True)
268 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
269 lieu
= models
.TextField()
270 debut
= models
.DateTimeField(default
=datetime
.datetime
.now
)
271 fin
= models
.DateTimeField(default
=datetime
.datetime
.now
)
272 pays
= models
.ForeignKey(Pays
, related_name
='evenements', null
=True, blank
=True)
273 fuseau
= models
.CharField(max_length
=100, choices
=TIME_ZONE_CHOICES
, verbose_name
='fuseau horaire')
274 description
= models
.TextField(blank
=True, null
=True)
275 contact
= models
.TextField(blank
=True, null
=True)
276 url
= models
.CharField(max_length
=255, blank
=True, null
=True)
277 piece_jointe
= models
.FileField(upload_to
='agenda/pj', blank
=True, verbose_name
='pièce jointe')
278 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="evenements", verbose_name
='régions')
280 objects
= EvenementManager()
281 all_objects
= models
.Manager()
284 ordering
= ['-debut']
286 def __unicode__(self
,):
287 return "[%s] %s" % (self
.uid
, self
.titre
)
289 def duration_display(self
):
290 delta
= self
.fin
- self
.debut
291 minutes
, seconds
= divmod(delta
.seconds
, 60)
292 hours
, minutes
= divmod(minutes
, 60)
296 parts
.append('1 jour')
298 parts
.append('%d jours' % days
)
300 parts
.append('1 heure')
302 parts
.append('%d heures' % hours
)
304 parts
.append('1 minute')
306 parts
.append('%d minutes' % minutes
)
307 return ' '.join(parts
)
309 def piece_jointe_display(self
):
310 return self
.piece_jointe
and os
.path
.basename(self
.piece_jointe
.name
)
313 from django
.core
.exceptions
import ValidationError
314 if self
.debut
> self
.fin
:
315 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
317 def save(self
, *args
, **kwargs
):
318 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
321 super(Evenement
, self
).save(*args
, **kwargs
)
324 # methodes de commnunications avec CALDAV
326 """Retourne l'evenement django sous forme d'objet icalendar"""
327 cal
= vobject
.iCalendar()
330 # fournit son propre uid
331 if self
.uid
in [None, ""]:
332 self
.uid
= str(uuid
.uuid1())
334 cal
.vevent
.add('uid').value
= self
.uid
336 cal
.vevent
.add('summary').value
= self
.titre
338 if self
.mots_cles
is None:
341 kw
= self
.mots_cles
.split(",")
344 kw
.append(self
.discipline
.nom
)
345 kw
.append(self
.discipline_secondaire
.nom
)
349 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
351 cal
.vevent
.add('x-auf-keywords').value
= k
353 description
= self
.description
355 if len(self
.description
) > 0:
357 description
+= u
"Mots-clés: " + ", ".join(kw
)
359 cal
.vevent
.add('dtstart').value
= combine(self
.debut
, pytz
.timezone(self
.fuseau
))
360 cal
.vevent
.add('dtend').value
= combine(self
.fin
, pytz
.timezone(self
.fuseau
))
361 cal
.vevent
.add('created').value
= combine(datetime
.datetime
.now(), "UTC")
362 cal
.vevent
.add('dtstamp').value
= combine(datetime
.datetime
.now(), "UTC")
363 if len(description
) > 0:
364 cal
.vevent
.add('description').value
= description
365 if len(self
.contact
) > 0:
366 cal
.vevent
.add('contact').value
= self
.contact
367 if len(self
.url
) > 0:
368 cal
.vevent
.add('url').value
= self
.url
369 if len(self
.lieu
) > 0:
370 cal
.vevent
.add('location').value
= self
.lieu
371 if self
.piece_jointe
:
372 url
= self
.piece_jointe
.url
373 if not url
.startswith('http://'):
374 url
= SITE_ROOT_URL
+ url
375 cal
.vevent
.add('attach').value
= url
378 def update_vevent(self
,):
379 """Essaie de créer l'évènement sur le serveur ical.
380 En cas de succès, l'évènement local devient donc inactif et approuvé"""
383 event
= self
.as_ical()
384 client
= caldav
.DAVClient(CALENDRIER_URL
)
385 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
386 e
= caldav
.Event(client
, parent
= cal
, data
= event
.serialize(), id=self
.uid
)
389 self
.approuve
= False
391 def delete_vevent(self
,):
392 """Supprime l'evenement sur le serveur caldav"""
395 event
= self
.as_ical()
396 client
= caldav
.DAVClient(CALENDRIER_URL
)
397 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
398 e
= cal
.event(self
.uid
)
400 except error
.NotFoundError
:
403 def assigner_regions(self
, regions
):
404 self
.regions
.add(*regions
)
406 def assigner_disciplines(self
, disciplines
):
407 if len(disciplines
) == 1:
409 self
.discipline_secondaire
= disciplines
[0]
411 self
.discipline
= disciplines
[0]
412 elif len(disciplines
) >= 2:
413 self
.discipline
= disciplines
[0]
414 self
.discipline_secondaire
= disciplines
[1]
416 def delete_vevent(sender
, instance
, *args
, **kwargs
):
417 # Surcharge du comportement de suppression
418 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
419 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
420 instance
.delete_vevent()
421 pre_delete
.connect(delete_vevent
, sender
=Evenement
)
425 class ListSet(models
.Model
):
426 spec
= models
.CharField(primary_key
= True, max_length
= 255)
427 name
= models
.CharField(max_length
= 255)
428 server
= models
.CharField(max_length
= 255)
429 validated
= models
.BooleanField(default
= True)
431 def __unicode__(self
,):
434 class RecordSphinxQuerySet(SEPSphinxQuerySet
):
436 def __init__(self
, model
=None):
437 SEPSphinxQuerySet
.__init__(self
, model
=model
, index
='savoirsenpartage_ressources',
438 weights
=dict(title
=3))
440 class RecordManager(SEPManager
):
442 def get_query_set(self
):
443 """Ne garder que les ressources validées et qui sont soit dans aucun
444 listset ou au moins dans un listset validé."""
445 qs
= SEPQuerySet(self
.model
)
446 qs
= qs
.filter(validated
=True)
447 qs
= qs
.filter(Q(listsets__isnull
=True) |
Q(listsets__validated
=True))
450 def get_sphinx_query_set(self
):
451 return RecordSphinxQuerySet(self
.model
)
453 class Record(models
.Model
):
455 #fonctionnement interne
456 id = models
.AutoField(primary_key
= True)
457 server
= models
.CharField(max_length
= 255, verbose_name
=u
'serveur')
458 last_update
= models
.CharField(max_length
= 255)
459 last_checksum
= models
.CharField(max_length
= 255)
460 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
463 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
464 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
465 description
= models
.TextField(null
=True, blank
=True)
466 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
467 identifier
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
468 uri
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
469 source
= models
.TextField(null
= True, blank
= True)
470 contributor
= models
.TextField(null
= True, blank
= True)
471 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
472 publisher
= models
.TextField(null
= True, blank
= True)
473 type = models
.TextField(null
= True, blank
= True)
474 format
= models
.TextField(null
= True, blank
= True)
475 language
= models
.TextField(null
= True, blank
= True)
477 listsets
= models
.ManyToManyField(ListSet
, null
= True, blank
= True)
479 #SEP 2 (aucune données récoltées)
480 alt_title
= models
.TextField(null
= True, blank
= True)
481 abstract
= models
.TextField(null
= True, blank
= True)
482 creation
= models
.CharField(max_length
= 255, null
= True, blank
= True)
483 issued
= models
.CharField(max_length
= 255, null
= True, blank
= True)
484 isbn
= models
.TextField(null
= True, blank
= True)
485 orig_lang
= models
.TextField(null
= True, blank
= True)
487 # Metadata AUF multivaluées
488 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
489 thematiques
= models
.ManyToManyField(Thematique
, blank
=True, verbose_name
='thématiques')
490 pays
= models
.ManyToManyField(Pays
, blank
=True)
491 regions
= models
.ManyToManyField(Region
, blank
=True, verbose_name
='régions')
494 objects
= RecordManager()
495 all_objects
= models
.Manager()
498 verbose_name
= 'ressource'
500 def __unicode__(self
):
501 return "[%s] %s" % (self
.server
, self
.title
)
503 def getServeurURL(self
):
504 """Retourne l'URL du serveur de provenance"""
505 return RESOURCES
[self
.server
]['url']
507 def est_complet(self
):
508 """teste si le record à toutes les données obligatoires"""
509 return self
.disciplines
.count() > 0 and \
510 self
.thematiques
.count() > 0 and \
511 self
.pays
.count() > 0 and \
512 self
.regions
.count() > 0
514 def assigner_regions(self
, regions
):
515 self
.regions
.add(*regions
)
517 def assigner_disciplines(self
, disciplines
):
518 self
.disciplines
.add(*disciplines
)
520 class Serveur(models
.Model
):
521 """Identification d'un serveur d'ou proviennent les références"""
522 nom
= models
.CharField(primary_key
= True, max_length
= 255)
524 def __unicode__(self
,):
527 def conf_2_db(self
,):
528 for k
in RESOURCES
.keys():
529 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
533 class Profile(models
.Model
):
534 user
= models
.ForeignKey(User
, unique
=True)
535 serveurs
= models
.ManyToManyField(Serveur
, null
= True, blank
= True)
537 class HarvestLog(models
.Model
):
538 context
= models
.CharField(max_length
= 255)
539 name
= models
.CharField(max_length
= 255)
540 date
= models
.DateTimeField(auto_now
= True)
541 added
= models
.IntegerField(null
= True, blank
= True)
542 updated
= models
.IntegerField(null
= True, blank
= True)
543 processed
= models
.IntegerField(null
= True, blank
= True)
544 record
= models
.ForeignKey(Record
, null
= True, blank
= True)
548 logger
= HarvestLog()
549 if message
.has_key('record_id'):
550 message
['record'] = Record
.all_objects
.get(id=message
['record_id'])
551 del(message
['record_id'])
553 for k
,v
in message
.items():
554 setattr(logger
, k
, v
)