1 # -*- encoding: utf-8 -*-
2 import simplejson
, uuid
, datetime
, caldav
, vobject
, uuid
, random
, operator
, pytz
3 from babel
.dates
import get_timezone_name
4 from django
.contrib
.auth
.models
import User
5 from django
.db
import models
6 from django
.db
.models
import Q
, Max
7 from django
.db
.models
.signals
import pre_delete
8 from auf_savoirs_en_partage
.backend_config
import RESOURCES
9 from savoirs
.globals import META
10 from settings
import CALENDRIER_URL
11 from datamaster_modeles
.models
import Thematique
, Pays
, Region
12 from lib
.calendrier
import combine
13 from caldav
.lib
import error
15 class RandomQuerySetMixin(object):
16 """Mixin pour les modèles.
18 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
19 méthode pour récupérer des objets au hasard.
22 def random(self
, n
=1):
23 """Récupère aléatoirement un nombre donné d'objets."""
24 ids
= random
.sample(xrange(self
.count()), n
)
25 return [self
[i
] for i
in ids
]
27 class Discipline(models
.Model
):
28 id = models
.IntegerField(primary_key
=True, db_column
='id_discipline')
29 nom
= models
.CharField(max_length
=765, db_column
='nom_discipline')
31 def __unicode__ (self
):
35 db_table
= u
'discipline'
38 class SourceActualite(models
.Model
):
39 nom
= models
.CharField(max_length
=255)
40 url
= models
.CharField(max_length
=255)
42 def __unicode__(self
,):
43 return u
"%s" % self
.nom
45 class ActualiteManager(models
.Manager
):
47 def get_query_set(self
):
48 return ActualiteQuerySet(self
.model
)
50 def search(self
, text
):
51 return self
.get_query_set().search(text
)
53 class ActualiteQuerySet(models
.query
.QuerySet
, RandomQuerySetMixin
):
55 def search(self
, text
):
57 for word
in text
.split():
58 part
= (Q(titre__icontains
=word
) |
Q(texte__icontains
=word
) |
59 Q(regions__nom__icontains
=word
) |
Q(disciplines__nom__icontains
=word
))
64 return self
.filter(q
).distinct() if q
is not None else self
66 class Actualite(models
.Model
):
67 id = models
.AutoField(primary_key
=True, db_column
='id_actualite')
68 titre
= models
.CharField(max_length
=765, db_column
='titre_actualite')
69 texte
= models
.TextField(db_column
='texte_actualite')
70 url
= models
.CharField(max_length
=765, db_column
='url_actualite')
71 date
= models
.DateField(db_column
='date_actualite')
72 visible
= models
.BooleanField(db_column
='visible_actualite', default
= False)
73 ancienid
= models
.IntegerField(db_column
='ancienId_actualite', blank
= True, null
= True)
74 source
= models
.ForeignKey(SourceActualite
, blank
= True, null
= True)
75 disciplines
= models
.ManyToManyField(Discipline
, blank
=True, related_name
="actualites")
76 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="actualites", verbose_name
='régions')
78 objects
= ActualiteManager()
81 db_table
= u
'actualite'
84 def __unicode__ (self
):
85 return "%s" % (self
.titre
)
87 def assigner_disciplines(self
, disciplines
):
88 self
.disciplines
.add(*disciplines
)
90 def assigner_regions(self
, regions
):
91 self
.regions
.add(*regions
)
93 class EvenementManager(models
.Manager
):
95 def get_query_set(self
):
96 return EvenementQuerySet(self
.model
)
98 def search(self
, text
):
99 return self
.get_query_set().search(text
)
101 class EvenementQuerySet(models
.query
.QuerySet
, RandomQuerySetMixin
):
103 def search(self
, text
):
105 for word
in text
.split():
106 part
= (Q(titre__icontains
=word
) |
107 Q(mots_cles__icontains
=word
) |
108 Q(discipline__nom__icontains
=word
) |
109 Q(discipline_secondaire__nom__icontains
=word
) |
110 Q(type__icontains
=word
) |
111 Q(lieu__icontains
=word
) |
112 Q(description__icontains
=word
) |
113 Q(contact__icontains
=word
) |
114 Q(regions__nom__icontains
=word
))
119 return self
.filter(q
).distinct() if q
is not None else self
121 def search_titre(self
, text
):
123 for word
in text
.split():
124 qs
= qs
.filter(titre__icontains
=word
)
127 def build_time_zone_choices():
130 now
= datetime
.datetime
.now()
131 for tzname
in pytz
.common_timezones
:
132 tz
= pytz
.timezone(tzname
)
133 fr_name
= get_timezone_name(tz
, locale
='fr_FR')
134 if fr_name
in fr_names
:
136 fr_names
.add(fr_name
)
137 offset
= tz
.utcoffset(now
)
138 seconds
= offset
.seconds
+ offset
.days
* 86400
139 (hours
, minutes
) = divmod(seconds
// 60, 60)
140 offset_str
= 'UTC%+d:%d' % (hours
, minutes
) if minutes
else 'UTC%+d' % hours
141 tzones
.append((seconds
, tzname
, '%s - %s' % (offset_str
, fr_name
)))
143 return [(tz
[1], tz
[2]) for tz
in tzones
]
145 class Evenement(models
.Model
):
146 TYPE_CHOICES
= ((u
'Colloque', u
'Colloque'),
147 (u
'Conférence', u
'Conférence'),
148 (u
'Appel à contribution', u
'Appel à contribution'),
149 (u
'Journée d\'étude', u
'Journée d\'étude'),
151 TIME_ZONE_CHOICES
= build_time_zone_choices()
153 uid
= models
.CharField(max_length
= 255, default
= str(uuid
.uuid1()))
154 approuve
= models
.BooleanField(default
=False, verbose_name
=u
'approuvé')
155 titre
= models
.CharField(max_length
=255)
156 discipline
= models
.ForeignKey('Discipline', related_name
= "discipline",
157 blank
= True, null
= True)
158 discipline_secondaire
= models
.ForeignKey('Discipline', related_name
="discipline_secondaire",
159 verbose_name
=u
"discipline secondaire",
160 blank
=True, null
=True)
161 mots_cles
= models
.TextField('Mots-Clés', blank
= True, null
= True)
162 type = models
.CharField(max_length
=255, choices
=TYPE_CHOICES
)
163 lieu
= models
.TextField()
164 debut
= models
.DateTimeField(default
= datetime
.datetime
.now
)
165 fin
= models
.DateTimeField(default
= datetime
.datetime
.now
)
166 fuseau
= models
.CharField(max_length
=100, choices
=TIME_ZONE_CHOICES
, verbose_name
='fuseau horaire')
167 description
= models
.TextField(blank
= True, null
= True)
169 contact
= models
.TextField(blank
= True, null
= True)
170 url
= models
.CharField(max_length
=255, blank
= True, null
= True)
171 regions
= models
.ManyToManyField(Region
, blank
=True, related_name
="evenements", verbose_name
='régions')
173 objects
= EvenementManager()
176 ordering
= ['-debut']
178 def __unicode__(self
,):
179 return "[%s] %s" % (self
.uid
, self
.titre
)
182 from django
.core
.exceptions
import ValidationError
183 if self
.debut
> self
.fin
:
184 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
186 def save(self
, *args
, **kwargs
):
187 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
191 super(Evenement
, self
).save(*args
, **kwargs
)
193 # methodes de commnunications avec CALDAV
195 """Retourne l'evenement django sous forme d'objet icalendar"""
196 cal
= vobject
.iCalendar()
199 # fournit son propre uid
200 if self
.uid
in [None, ""]:
201 self
.uid
= str(uuid
.uuid1())
203 cal
.vevent
.add('uid').value
= self
.uid
205 cal
.vevent
.add('summary').value
= self
.titre
207 if self
.mots_cles
is None:
210 kw
= self
.mots_cles
.split(",")
213 kw
.append(self
.discipline
.nom
)
214 kw
.append(self
.discipline_secondaire
.nom
)
218 kw
= [x
.strip() for x
in kw
if len(x
.strip()) > 0 and x
is not None]
220 cal
.vevent
.add('x-auf-keywords').value
= k
222 description
= self
.description
224 if len(self
.description
) > 0:
226 description
+= u
"Mots-clés: " + ", ".join(kw
)
228 cal
.vevent
.add('dtstart').value
= combine(self
.debut
, pytz
.timezone(self
.fuseau
))
229 cal
.vevent
.add('dtend').value
= combine(self
.fin
, pytz
.timezone(self
.fuseau
))
230 cal
.vevent
.add('created').value
= combine(datetime
.datetime
.now(), "UTC")
231 cal
.vevent
.add('dtstamp').value
= combine(datetime
.datetime
.now(), "UTC")
232 if len(description
) > 0:
233 cal
.vevent
.add('description').value
= description
234 if len(self
.contact
) > 0:
235 cal
.vevent
.add('contact').value
= self
.contact
236 if len(self
.url
) > 0:
237 cal
.vevent
.add('url').value
= self
.url
238 if len(self
.lieu
) > 0:
239 cal
.vevent
.add('location').value
= self
.lieu
242 def update_vevent(self
,):
243 """Essaie de créer l'évènement sur le serveur ical.
244 En cas de succès, l'évènement local devient donc inactif et approuvé"""
247 event
= self
.as_ical()
248 client
= caldav
.DAVClient(CALENDRIER_URL
)
249 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
250 e
= caldav
.Event(client
, parent
= cal
, data
= event
.serialize(), id=self
.uid
)
253 self
.approuve
= False
255 def delete_vevent(self
,):
256 """Supprime l'evenement sur le serveur caldav"""
259 event
= self
.as_ical()
260 client
= caldav
.DAVClient(CALENDRIER_URL
)
261 cal
= caldav
.Calendar(client
, url
= CALENDRIER_URL
)
262 e
= cal
.event(self
.uid
)
264 except error
.NotFoundError
:
267 def assigner_regions(self
, regions
):
268 self
.regions
.add(*regions
)
270 def assigner_disciplines(self
, disciplines
):
271 if len(disciplines
) == 1:
273 self
.discipline_secondaire
= disciplines
[0]
275 self
.discipline
= disciplines
[0]
276 elif len(disciplines
) >= 2:
277 self
.discipline
= disciplines
[0]
278 self
.discipline_secondaire
= disciplines
[1]
281 # Surcharge du comportement de suppression
282 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
283 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
284 def delete_vevent(sender
, instance
, *args
, **kwargs
):
285 instance
.delete_vevent()
287 pre_delete
.connect(delete_vevent
, sender
= Evenement
)
290 class ListSet(models
.Model
):
291 spec
= models
.CharField(primary_key
= True, max_length
= 255)
292 name
= models
.CharField(max_length
= 255)
293 server
= models
.CharField(max_length
= 255)
294 validated
= models
.BooleanField(default
= True)
296 def __unicode__(self
,):
299 class RecordManager(models
.Manager
):
301 def get_query_set(self
):
302 return RecordQuerySet(self
.model
)
304 def search(self
, text
):
305 return self
.get_query_set().search(text
)
308 return self
.get_query_set().validated()
310 class RecordQuerySet(models
.query
.QuerySet
, RandomQuerySetMixin
):
312 def search(self
, text
):
316 # Ne garder que les ressources qui contiennent tous les mots
320 matching_pays
= list(Pays
.objects
.filter(Q(nom__icontains
=word
) |
Q(region__nom__icontains
=word
)).values_list('pk', flat
=True))
321 part
= (Q(title__icontains
=word
) |
Q(description__icontains
=word
) |
322 Q(creator__icontains
=word
) |
Q(contributor__icontains
=word
) |
323 Q(subject__icontains
=word
) |
Q(disciplines__nom__icontains
=word
) |
324 Q(regions__nom__icontains
=word
) |
Q(pays__in
=matching_pays
) |
325 Q(publisher__icontains
=word
))
331 qs
= qs
.filter(q
).distinct()
333 # On donne un point pour chaque mot présent dans le titre.
335 score_expr
= ' + '.join(['(title LIKE %s)'] * len(words
))
336 score_params
= ['%' + word
+ '%' for word
in words
]
338 select
={'score': score_expr
},
339 select_params
=score_params
343 def search_auteur(self
, text
):
345 for word
in text
.split():
346 qs
= qs
.filter(Q(creator__icontains
=word
) |
Q(contributor__icontains
=word
))
349 def search_sujet(self
, text
):
351 for word
in text
.split():
352 qs
= qs
.filter(subject__icontains
=word
)
355 def search_titre(self
, text
):
357 for word
in text
.split():
358 qs
= qs
.filter(title__icontains
=word
)
362 """Ne garder que les ressources validées et qui sont soit dans aucun
363 listset ou au moins dans un listset validé."""
364 qs
= self
.filter(validated
=True)
365 qs
= qs
.filter(Q(listsets__isnull
=True) |
Q(listsets__validated
=True))
368 def filter(self
, *args
, **kwargs
):
369 """Gère des filtres supplémentaires pour l'admin.
371 C'est la seule façon que j'ai trouvée de contourner les mécanismes
372 de recherche de l'admin."""
373 search
= kwargs
.pop('admin_search', None)
374 search_titre
= kwargs
.pop('admin_search_titre', None)
375 search_sujet
= kwargs
.pop('admin_search_sujet', None)
376 search_description
= kwargs
.pop('admin_search_description', None)
377 search_auteur
= kwargs
.pop('admin_search_auteur', None)
381 search_all
= not (search_titre
or search_description
or search_sujet
or search_auteur
)
383 if search_titre
or search_all
:
384 fields
+= ['title', 'alt_title']
385 if search_description
or search_all
:
386 fields
+= ['description', 'abstract']
387 if search_sujet
or search_all
:
388 fields
+= ['subject']
389 if search_auteur
or search_all
:
390 fields
+= ['creator', 'contributor']
392 for bit
in search
.split():
393 or_queries
= [Q(**{field
+ '__icontains': bit
}) for field
in fields
]
394 qs
= qs
.filter(reduce(operator
.or_
, or_queries
))
397 qs
= super(RecordQuerySet
, qs
).filter(*args
, **kwargs
)
400 return super(RecordQuerySet
, self
).filter(*args
, **kwargs
)
404 class Record(models
.Model
):
406 #fonctionnement interne
407 id = models
.AutoField(primary_key
= True)
408 server
= models
.CharField(max_length
= 255, verbose_name
=u
'serveur')
409 last_update
= models
.CharField(max_length
= 255)
410 last_checksum
= models
.CharField(max_length
= 255)
411 validated
= models
.BooleanField(default
=True, verbose_name
=u
'validé')
414 title
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'titre')
415 creator
= models
.TextField(null
=True, blank
=True, verbose_name
=u
'auteur')
416 description
= models
.TextField(null
=True, blank
=True)
417 modified
= models
.CharField(max_length
=255, null
=True, blank
=True)
418 identifier
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
419 uri
= models
.CharField(max_length
= 255, null
= True, blank
= True, unique
= True)
420 source
= models
.TextField(null
= True, blank
= True)
421 contributor
= models
.TextField(null
= True, blank
= True)
422 subject
= models
.TextField(null
=True, blank
=True, verbose_name
='sujet')
423 publisher
= models
.TextField(null
= True, blank
= True)
424 type = models
.TextField(null
= True, blank
= True)
425 format
= models
.TextField(null
= True, blank
= True)
426 language
= models
.TextField(null
= True, blank
= True)
428 listsets
= models
.ManyToManyField(ListSet
, null
= True, blank
= True)
430 #SEP 2 (aucune données récoltées)
431 alt_title
= models
.TextField(null
= True, blank
= True)
432 abstract
= models
.TextField(null
= True, blank
= True)
433 creation
= models
.CharField(max_length
= 255, null
= True, blank
= True)
434 issued
= models
.CharField(max_length
= 255, null
= True, blank
= True)
435 isbn
= models
.TextField(null
= True, blank
= True)
436 orig_lang
= models
.TextField(null
= True, blank
= True)
438 # Metadata AUF multivaluées
439 disciplines
= models
.ManyToManyField(Discipline
, blank
=True)
440 thematiques
= models
.ManyToManyField(Thematique
, blank
=True, verbose_name
='thématiques')
441 pays
= models
.ManyToManyField(Pays
, blank
=True)
442 regions
= models
.ManyToManyField(Region
, blank
=True, verbose_name
='régions')
445 objects
= RecordManager()
448 verbose_name
= 'ressource'
450 def __unicode__(self
):
451 return "[%s] %s" % (self
.server
, self
.title
)
453 def getServeurURL(self
):
454 """Retourne l'URL du serveur de provenance"""
455 return RESOURCES
[self
.server
]['url']
457 def est_complet(self
):
458 """teste si le record à toutes les données obligatoires"""
459 return self
.disciplines
.count() > 0 and \
460 self
.thematiques
.count() > 0 and \
461 self
.pays
.count() > 0 and \
462 self
.regions
.count() > 0
464 def assigner_regions(self
, regions
):
465 self
.regions
.add(*regions
)
467 def assigner_disciplines(self
, disciplines
):
468 self
.disciplines
.add(*disciplines
)
471 class Serveur(models
.Model
):
472 """Identification d'un serveur d'ou proviennent les références"""
473 nom
= models
.CharField(primary_key
= True, max_length
= 255)
475 def __unicode__(self
,):
478 def conf_2_db(self
,):
479 for k
in RESOURCES
.keys():
480 s
, created
= Serveur
.objects
.get_or_create(nom
=k
)
484 class Profile(models
.Model
):
485 user
= models
.ForeignKey(User
, unique
=True)
486 serveurs
= models
.ManyToManyField(Serveur
, null
= True, blank
= True)
488 class HarvestLog(models
.Model
):
489 context
= models
.CharField(max_length
= 255)
490 name
= models
.CharField(max_length
= 255)
491 date
= models
.DateTimeField(auto_now
= True)
492 added
= models
.IntegerField(null
= True, blank
= True)
493 updated
= models
.IntegerField(null
= True, blank
= True)
494 processed
= models
.IntegerField(null
= True, blank
= True)
495 record
= models
.ForeignKey(Record
, null
= True, blank
= True)
499 logger
= HarvestLog()
500 if message
.has_key('record_id'):
501 message
['record'] = Record
.objects
.get(id=message
['record_id'])
502 del(message
['record_id'])
504 for k
,v
in message
.items():
505 setattr(logger
, k
, v
)