Meilleure gestion des fuseaux horaires lors de l'ajout d'un événement.
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
CommitLineData
92c7413b 1# -*- encoding: utf-8 -*-
86983865
EMS
2import simplejson, uuid, datetime, caldav, vobject, uuid, random, operator, pytz
3from babel.dates import get_timezone_name
6d885e0c 4from django.contrib.auth.models import User
d15017b2 5from django.db import models
15261361 6from django.db.models import Q, Max
b7a741ad 7from django.db.models.signals import pre_delete
92c7413b 8from timezones.fields import TimeZoneField
6d885e0c 9from auf_savoirs_en_partage.backend_config import RESOURCES
da9020f3 10from savoirs.globals import META
b7a741ad 11from settings import CALENDRIER_URL
e3c3296e 12from datamaster_modeles.models import Thematique, Pays, Region
b7a741ad 13from lib.calendrier import combine
14from caldav.lib import error
d15017b2 15
15261361
EMS
16class RandomQuerySetMixin(object):
17 """Mixin pour les modèles.
18
19 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
20 méthode pour récupérer des objets au hasard.
21 """
22
23 def random(self, n=1):
24 """Récupère aléatoirement un nombre donné d'objets."""
25 ids = random.sample(xrange(self.count()), n)
26 return [self[i] for i in ids]
27
d15017b2
CR
28class Discipline(models.Model):
29 id = models.IntegerField(primary_key=True, db_column='id_discipline')
30 nom = models.CharField(max_length=765, db_column='nom_discipline')
6ef8ead4
CR
31
32 def __unicode__ (self):
92c7413b 33 return self.nom
6ef8ead4 34
d15017b2
CR
35 class Meta:
36 db_table = u'discipline'
37 ordering = ["nom",]
38
79c398f6
CR
39class SourceActualite(models.Model):
40 nom = models.CharField(max_length=255)
41 url = models.CharField(max_length=255)
ccbc4363 42
43 def __unicode__(self,):
44 return u"%s" % self.nom
79c398f6 45
2f9c4d6c
EMS
46class ActualiteManager(models.Manager):
47
48 def get_query_set(self):
49 return ActualiteQuerySet(self.model)
50
da44ce68
EMS
51 def search(self, text):
52 return self.get_query_set().search(text)
53
15261361 54class ActualiteQuerySet(models.query.QuerySet, RandomQuerySetMixin):
2f9c4d6c
EMS
55
56 def search(self, text):
82f25472
EMS
57 q = None
58 for word in text.split():
59 part = (Q(titre__icontains=word) | Q(texte__icontains=word) |
60 Q(regions__nom__icontains=word) | Q(disciplines__nom__icontains=word))
61 if q is None:
62 q = part
63 else:
64 q = q & part
65 return self.filter(q).distinct() if q is not None else self
2f9c4d6c 66
d15017b2 67class Actualite(models.Model):
4f262f90 68 id = models.AutoField(primary_key=True, db_column='id_actualite')
d15017b2
CR
69 titre = models.CharField(max_length=765, db_column='titre_actualite')
70 texte = models.TextField(db_column='texte_actualite')
71 url = models.CharField(max_length=765, db_column='url_actualite')
d15017b2 72 date = models.DateField(db_column='date_actualite')
f554ef70 73 visible = models.BooleanField(db_column='visible_actualite', default = False)
3b6456b0 74 ancienid = models.IntegerField(db_column='ancienId_actualite', blank = True, null = True)
ccbc4363 75 source = models.ForeignKey(SourceActualite, blank = True, null = True)
3a45eb64 76 disciplines = models.ManyToManyField(Discipline, blank=True, related_name="actualites")
a5f76eb4 77 regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
6ef8ead4 78
2f9c4d6c
EMS
79 objects = ActualiteManager()
80
d15017b2
CR
81 class Meta:
82 db_table = u'actualite'
83 ordering = ["-date",]
92c7413b 84
264a3210
EMS
85 def __unicode__ (self):
86 return "%s" % (self.titre)
87
88 def assigner_disciplines(self, disciplines):
89 self.disciplines.add(*disciplines)
90
91 def assigner_regions(self, regions):
92 self.regions.add(*regions)
93
4101cfc0
EMS
94class EvenementManager(models.Manager):
95
96 def get_query_set(self):
97 return EvenementQuerySet(self.model)
98
99 def search(self, text):
100 return self.get_query_set().search(text)
101
15261361 102class EvenementQuerySet(models.query.QuerySet, RandomQuerySetMixin):
4101cfc0
EMS
103
104 def search(self, text):
0b1ddc11
EMS
105 q = None
106 for word in text.split():
107 part = (Q(titre__icontains=word) |
108 Q(mots_cles__icontains=word) |
109 Q(discipline__nom__icontains=word) |
110 Q(discipline_secondaire__nom__icontains=word) |
111 Q(type__icontains=word) |
112 Q(lieu__icontains=word) |
113 Q(description__icontains=word) |
82f25472
EMS
114 Q(contact__icontains=word) |
115 Q(regions__nom__icontains=word))
0b1ddc11
EMS
116 if q is None:
117 q = part
118 else:
119 q = q & part
82f25472 120 return self.filter(q).distinct() if q is not None else self
4101cfc0 121
7bbf600c
EMS
122 def search_titre(self, text):
123 qs = self
124 for word in text.split():
125 qs = qs.filter(titre__icontains=word)
126 return qs
127
86983865
EMS
128def build_time_zone_choices():
129 fr_names = set()
130 tzones = []
131 now = datetime.datetime.now()
132 for tzname in pytz.common_timezones:
133 tz = pytz.timezone(tzname)
134 fr_name = get_timezone_name(tz, locale='fr_FR')
135 if fr_name in fr_names:
136 continue
137 fr_names.add(fr_name)
138 offset = tz.utcoffset(now)
139 seconds = offset.seconds + offset.days * 86400
140 (hours, minutes) = divmod(seconds // 60, 60)
141 offset_str = 'UTC%+d:%d' % (hours, minutes) if minutes else 'UTC%+d' % hours
142 tzones.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
143 tzones.sort()
144 return [(tz[1], tz[2]) for tz in tzones]
145
92c7413b 146class Evenement(models.Model):
7bbf600c
EMS
147 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
148 (u'Conférence', u'Conférence'),
149 (u'Appel à contribution', u'Appel à contribution'),
150 (u'Journée d\'étude', u'Journée d\'étude'),
151 (None, u'Autre'))
86983865
EMS
152 TIME_ZONE_CHOICES = build_time_zone_choices()
153
c5b3da8b 154 uid = models.CharField(max_length = 255, default = str(uuid.uuid1()))
a5f76eb4 155 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
92c7413b
CR
156 titre = models.CharField(max_length=255)
157 discipline = models.ForeignKey('Discipline', related_name = "discipline",
158 blank = True, null = True)
a5f76eb4
EMS
159 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
160 verbose_name=u"discipline secondaire",
161 blank=True, null=True)
92c7413b 162 mots_cles = models.TextField('Mots-Clés', blank = True, null = True)
7bbf600c 163 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
86983865 164 lieu = models.TextField()
92c7413b
CR
165 debut = models.DateTimeField(default = datetime.datetime.now)
166 fin = models.DateTimeField(default = datetime.datetime.now)
86983865 167 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
92c7413b
CR
168 description = models.TextField(blank = True, null = True)
169 #fichiers = TODO?
170 contact = models.TextField(blank = True, null = True)
171 url = models.CharField(max_length=255, blank = True, null = True)
a5f76eb4 172 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
92c7413b 173
4101cfc0
EMS
174 objects = EvenementManager()
175
176 class Meta:
177 ordering = ['-debut']
178
020f79a9 179 def __unicode__(self,):
180 return "[%s] %s" % (self.uid, self.titre)
181
73309469 182 def clean(self):
183 from django.core.exceptions import ValidationError
184 if self.debut > self.fin:
185 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
186
b7a741ad 187 def save(self, *args, **kwargs):
188 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
189 approuvé"""
73309469 190 self.clean()
b7a741ad 191 self.update_vevent()
192 super(Evenement, self).save(*args, **kwargs)
193
194 # methodes de commnunications avec CALDAV
195 def as_ical(self,):
196 """Retourne l'evenement django sous forme d'objet icalendar"""
197 cal = vobject.iCalendar()
198 cal.add('vevent')
199
200 # fournit son propre uid
7f56d0d4 201 if self.uid in [None, ""]:
b7a741ad 202 self.uid = str(uuid.uuid1())
203
204 cal.vevent.add('uid').value = self.uid
205
206 cal.vevent.add('summary').value = self.titre
207
208 if self.mots_cles is None:
209 kw = []
210 else:
211 kw = self.mots_cles.split(",")
212
213 try:
214 kw.append(self.discipline.nom)
215 kw.append(self.discipline_secondaire.nom)
216 kw.append(self.type)
217 except: pass
218
79b400f0 219 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
b7a741ad 220 for k in kw:
221 cal.vevent.add('x-auf-keywords').value = k
222
223 description = self.description
224 if len(kw) > 0:
225 if len(self.description) > 0:
226 description += "\n"
028f548f 227 description += u"Mots-clés: " + ", ".join(kw)
b7a741ad 228
229 cal.vevent.add('dtstart').value = combine(self.debut, self.fuseau)
230 cal.vevent.add('dtend').value = combine(self.fin, self.fuseau)
231 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
232 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
79b400f0 233 if len(description) > 0:
b7a741ad 234 cal.vevent.add('description').value = description
235 if len(self.contact) > 0:
236 cal.vevent.add('contact').value = self.contact
237 if len(self.url) > 0:
238 cal.vevent.add('url').value = self.url
239 if len(self.lieu) > 0:
240 cal.vevent.add('location').value = self.lieu
241 return cal
242
243 def update_vevent(self,):
244 """Essaie de créer l'évènement sur le serveur ical.
245 En cas de succès, l'évènement local devient donc inactif et approuvé"""
246 try:
247 if self.approuve:
248 event = self.as_ical()
249 client = caldav.DAVClient(CALENDRIER_URL)
250 cal = caldav.Calendar(client, url = CALENDRIER_URL)
251 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
252 e.save()
253 except:
254 self.approuve = False
255
256 def delete_vevent(self,):
257 """Supprime l'evenement sur le serveur caldav"""
258 try:
259 if self.approuve:
260 event = self.as_ical()
261 client = caldav.DAVClient(CALENDRIER_URL)
262 cal = caldav.Calendar(client, url = CALENDRIER_URL)
263 e = cal.event(self.uid)
264 e.delete()
265 except error.NotFoundError:
266 pass
267
264a3210
EMS
268 def assigner_regions(self, regions):
269 self.regions.add(*regions)
270
271 def assigner_disciplines(self, disciplines):
272 if len(disciplines) == 1:
273 if self.discipline:
274 self.discipline_secondaire = disciplines[0]
275 else:
276 self.discipline = disciplines[0]
277 elif len(disciplines) >= 2:
278 self.discipline = disciplines[0]
279 self.discipline_secondaire = disciplines[1]
280
b7a741ad 281
282# Surcharge du comportement de suppression
283# La méthode de connexion par signals est préférable à surcharger la méthode delete()
284# car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
285def delete_vevent(sender, instance, *args, **kwargs):
286 instance.delete_vevent()
287
288pre_delete.connect(delete_vevent, sender = Evenement)
289
290
d972b61d 291class ListSet(models.Model):
292 spec = models.CharField(primary_key = True, max_length = 255)
293 name = models.CharField(max_length = 255)
294 server = models.CharField(max_length = 255)
9eda5d6c 295 validated = models.BooleanField(default = True)
d972b61d 296
10d37e44 297 def __unicode__(self,):
298 return self.name
299
da44ce68
EMS
300class RecordManager(models.Manager):
301
302 def get_query_set(self):
303 return RecordQuerySet(self.model)
304
305 def search(self, text):
306 return self.get_query_set().search(text)
307
f153be1b
EMS
308 def validated(self):
309 return self.get_query_set().validated()
310
15261361 311class RecordQuerySet(models.query.QuerySet, RandomQuerySetMixin):
da44ce68
EMS
312
313 def search(self, text):
f12cc7fb 314 qs = self
da44ce68 315 words = text.split()
da44ce68 316
f12cc7fb
EMS
317 # Ne garder que les ressources qui contiennent tous les mots
318 # demandés.
0b1ddc11 319 q = None
f12cc7fb 320 for word in words:
82f25472 321 matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
0b1ddc11
EMS
322 part = (Q(title__icontains=word) | Q(description__icontains=word) |
323 Q(creator__icontains=word) | Q(contributor__icontains=word) |
324 Q(subject__icontains=word) | Q(disciplines__nom__icontains=word) |
82f25472 325 Q(regions__nom__icontains=word) | Q(pays__in=matching_pays))
0b1ddc11
EMS
326 if q is None:
327 q = part
328 else:
329 q = q & part
3c23982e
EMS
330 if q is not None:
331 qs = qs.filter(q).distinct()
f12cc7fb
EMS
332
333 # On donne un point pour chaque mot présent dans le titre.
3c23982e
EMS
334 if words:
335 score_expr = ' + '.join(['(title LIKE %s)'] * len(words))
336 score_params = ['%' + word + '%' for word in words]
337 qs = qs.extra(
338 select={'score': score_expr},
339 select_params=score_params
340 ).order_by('-score')
3c23982e 341 return qs
f12cc7fb
EMS
342
343 def search_auteur(self, text):
344 qs = self
345 for word in text.split():
346 qs = qs.filter(Q(creator__icontains=word) | Q(contributor__icontains=word))
347 return qs
348
349 def search_sujet(self, text):
350 qs = self
351 for word in text.split():
352 qs = qs.filter(subject__icontains=word)
353 return qs
354
355 def search_titre(self, text):
356 qs = self
357 for word in text.split():
358 qs = qs.filter(title__icontains=word)
359 return qs
360
f153be1b
EMS
361 def validated(self):
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)
82f25472
EMS
365 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
366 return qs.distinct()
f153be1b 367
77b0fac0
EMS
368 def filter(self, *args, **kwargs):
369 """Gère des filtres supplémentaires pour l'admin.
370
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)
378
379 if search:
380 qs = self
381 search_all = not (search_titre or search_description or search_sujet or search_auteur)
382 fields = []
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']
391
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))
395
396 if args or kwargs:
397 qs = super(RecordQuerySet, qs).filter(*args, **kwargs)
398 return qs
399 else:
400 return super(RecordQuerySet, self).filter(*args, **kwargs)
401
402
403
0cc5f772 404class Record(models.Model):
23b5b3d5 405
406 #fonctionnement interne
0cc5f772 407 id = models.AutoField(primary_key = True)
a5f76eb4 408 server = models.CharField(max_length = 255, verbose_name=u'serveur')
23b5b3d5 409 last_update = models.CharField(max_length = 255)
410 last_checksum = models.CharField(max_length = 255)
a5f76eb4 411 validated = models.BooleanField(default=True, verbose_name=u'validé')
23b5b3d5 412
413 #OAI
18dbd2cf
EMS
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)
23b5b3d5 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)
18dbd2cf 422 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
23b5b3d5 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)
da9020f3 427
c88d78dc 428 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
d972b61d 429
da9020f3 430 #SEP 2 (aucune données récoltées)
23b5b3d5 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)
da9020f3 437
438 # Metadata AUF multivaluées
a342f93a
EMS
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')
0cc5f772 443
da44ce68
EMS
444 # Manager
445 objects = RecordManager()
446
18dbd2cf
EMS
447 class Meta:
448 verbose_name = 'ressource'
449
264a3210
EMS
450 def __unicode__(self):
451 return "[%s] %s" % (self.server, self.title)
452
453 def getServeurURL(self):
f98ad449 454 """Retourne l'URL du serveur de provenance"""
455 return RESOURCES[self.server]['url']
456
264a3210 457 def est_complet(self):
6d885e0c 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
463
264a3210
EMS
464 def assigner_regions(self, regions):
465 self.regions.add(*regions)
da9020f3 466
264a3210
EMS
467 def assigner_disciplines(self, disciplines):
468 self.disciplines.add(*disciplines)
469
470
6d885e0c 471class Serveur(models.Model):
b7a741ad 472 """Identification d'un serveur d'ou proviennent les références"""
6d885e0c 473 nom = models.CharField(primary_key = True, max_length = 255)
474
475 def __unicode__(self,):
476 return self.nom
477
478 def conf_2_db(self,):
479 for k in RESOURCES.keys():
480 s, created = Serveur.objects.get_or_create(nom=k)
481 s.nom = k
482 s.save()
483
484class Profile(models.Model):
485 user = models.ForeignKey(User, unique=True)
486 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
0cc5f772
CR
487
488class HarvestLog(models.Model):
23b5b3d5 489 context = models.CharField(max_length = 255)
490 name = models.CharField(max_length = 255)
0cc5f772 491 date = models.DateTimeField(auto_now = True)
23b5b3d5 492 added = models.IntegerField(null = True, blank = True)
493 updated = models.IntegerField(null = True, blank = True)
a85ba76e 494 processed = models.IntegerField(null = True, blank = True)
23b5b3d5 495 record = models.ForeignKey(Record, null = True, blank = True)
496
497 @staticmethod
498 def add(message):
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'])
503
504 for k,v in message.items():
505 setattr(logger, k, v)
506 logger.save()