Changé les questions relatives aux activités en Francophonie
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
CommitLineData
92c7413b 1# -*- encoding: utf-8 -*-
27fe0d70 2import simplejson, uuid, datetime, caldav, vobject, uuid, random, operator, pytz, os
86983865 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
6d885e0c 8from auf_savoirs_en_partage.backend_config import RESOURCES
da9020f3 9from savoirs.globals import META
74b087e5 10from settings import CALENDRIER_URL, SITE_ROOT_URL
e3c3296e 11from datamaster_modeles.models import Thematique, Pays, Region
b7a741ad 12from lib.calendrier import combine
13from caldav.lib import error
d15017b2 14
15261361
EMS
15class RandomQuerySetMixin(object):
16 """Mixin pour les modèles.
17
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.
20 """
21
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]
26
d15017b2
CR
27class 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')
6ef8ead4
CR
30
31 def __unicode__ (self):
92c7413b 32 return self.nom
6ef8ead4 33
d15017b2
CR
34 class Meta:
35 db_table = u'discipline'
36 ordering = ["nom",]
37
79c398f6
CR
38class SourceActualite(models.Model):
39 nom = models.CharField(max_length=255)
40 url = models.CharField(max_length=255)
ccbc4363 41
42 def __unicode__(self,):
43 return u"%s" % self.nom
79c398f6 44
2f9c4d6c
EMS
45class ActualiteManager(models.Manager):
46
47 def get_query_set(self):
48 return ActualiteQuerySet(self.model)
49
da44ce68
EMS
50 def search(self, text):
51 return self.get_query_set().search(text)
52
15261361 53class ActualiteQuerySet(models.query.QuerySet, RandomQuerySetMixin):
2f9c4d6c
EMS
54
55 def search(self, text):
82f25472
EMS
56 q = None
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))
60 if q is None:
61 q = part
62 else:
63 q = q & part
64 return self.filter(q).distinct() if q is not None else self
2f9c4d6c 65
d15017b2 66class Actualite(models.Model):
4f262f90 67 id = models.AutoField(primary_key=True, db_column='id_actualite')
d15017b2
CR
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')
d15017b2 71 date = models.DateField(db_column='date_actualite')
f554ef70 72 visible = models.BooleanField(db_column='visible_actualite', default = False)
3b6456b0 73 ancienid = models.IntegerField(db_column='ancienId_actualite', blank = True, null = True)
ccbc4363 74 source = models.ForeignKey(SourceActualite, blank = True, null = True)
3a45eb64 75 disciplines = models.ManyToManyField(Discipline, blank=True, related_name="actualites")
a5f76eb4 76 regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
6ef8ead4 77
2f9c4d6c
EMS
78 objects = ActualiteManager()
79
d15017b2
CR
80 class Meta:
81 db_table = u'actualite'
82 ordering = ["-date",]
92c7413b 83
264a3210
EMS
84 def __unicode__ (self):
85 return "%s" % (self.titre)
86
87 def assigner_disciplines(self, disciplines):
88 self.disciplines.add(*disciplines)
89
90 def assigner_regions(self, regions):
91 self.regions.add(*regions)
92
4101cfc0
EMS
93class EvenementManager(models.Manager):
94
95 def get_query_set(self):
96 return EvenementQuerySet(self.model)
97
98 def search(self, text):
99 return self.get_query_set().search(text)
100
15261361 101class EvenementQuerySet(models.query.QuerySet, RandomQuerySetMixin):
4101cfc0
EMS
102
103 def search(self, text):
0b1ddc11
EMS
104 q = None
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) |
82f25472
EMS
113 Q(contact__icontains=word) |
114 Q(regions__nom__icontains=word))
0b1ddc11
EMS
115 if q is None:
116 q = part
117 else:
118 q = q & part
82f25472 119 return self.filter(q).distinct() if q is not None else self
4101cfc0 120
7bbf600c
EMS
121 def search_titre(self, text):
122 qs = self
123 for word in text.split():
124 qs = qs.filter(titre__icontains=word)
125 return qs
126
86983865
EMS
127def build_time_zone_choices():
128 fr_names = set()
129 tzones = []
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:
135 continue
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)))
142 tzones.sort()
143 return [(tz[1], tz[2]) for tz in tzones]
144
92c7413b 145class Evenement(models.Model):
7bbf600c
EMS
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'),
150 (None, u'Autre'))
86983865
EMS
151 TIME_ZONE_CHOICES = build_time_zone_choices()
152
74b087e5 153 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
a5f76eb4 154 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
92c7413b
CR
155 titre = models.CharField(max_length=255)
156 discipline = models.ForeignKey('Discipline', related_name = "discipline",
157 blank = True, null = True)
a5f76eb4
EMS
158 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
159 verbose_name=u"discipline secondaire",
160 blank=True, null=True)
74b087e5 161 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
7bbf600c 162 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
86983865 163 lieu = models.TextField()
74b087e5
EMS
164 debut = models.DateTimeField(default=datetime.datetime.now)
165 fin = models.DateTimeField(default=datetime.datetime.now)
86983865 166 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
74b087e5
EMS
167 description = models.TextField(blank=True, null=True)
168 contact = models.TextField(blank=True, null=True)
169 url = models.CharField(max_length=255, blank=True, null=True)
170 piece_jointe = models.FileField(upload_to='agenda/pj', blank=True, verbose_name='pièce jointe')
a5f76eb4 171 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
92c7413b 172
4101cfc0
EMS
173 objects = EvenementManager()
174
175 class Meta:
176 ordering = ['-debut']
177
020f79a9 178 def __unicode__(self,):
179 return "[%s] %s" % (self.uid, self.titre)
180
27fe0d70
EMS
181 def piece_jointe_display(self):
182 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
183
73309469 184 def clean(self):
185 from django.core.exceptions import ValidationError
186 if self.debut > self.fin:
187 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
188
b7a741ad 189 def save(self, *args, **kwargs):
190 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
191 approuvé"""
73309469 192 self.clean()
b7a741ad 193 self.update_vevent()
194 super(Evenement, self).save(*args, **kwargs)
195
196 # methodes de commnunications avec CALDAV
197 def as_ical(self,):
198 """Retourne l'evenement django sous forme d'objet icalendar"""
199 cal = vobject.iCalendar()
200 cal.add('vevent')
201
202 # fournit son propre uid
7f56d0d4 203 if self.uid in [None, ""]:
b7a741ad 204 self.uid = str(uuid.uuid1())
205
206 cal.vevent.add('uid').value = self.uid
207
208 cal.vevent.add('summary').value = self.titre
209
210 if self.mots_cles is None:
211 kw = []
212 else:
213 kw = self.mots_cles.split(",")
214
215 try:
216 kw.append(self.discipline.nom)
217 kw.append(self.discipline_secondaire.nom)
218 kw.append(self.type)
219 except: pass
220
79b400f0 221 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
b7a741ad 222 for k in kw:
223 cal.vevent.add('x-auf-keywords').value = k
224
225 description = self.description
226 if len(kw) > 0:
227 if len(self.description) > 0:
228 description += "\n"
028f548f 229 description += u"Mots-clés: " + ", ".join(kw)
b7a741ad 230
7f214e0f
EMS
231 cal.vevent.add('dtstart').value = combine(self.debut, pytz.timezone(self.fuseau))
232 cal.vevent.add('dtend').value = combine(self.fin, pytz.timezone(self.fuseau))
b7a741ad 233 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
234 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
79b400f0 235 if len(description) > 0:
b7a741ad 236 cal.vevent.add('description').value = description
237 if len(self.contact) > 0:
238 cal.vevent.add('contact').value = self.contact
239 if len(self.url) > 0:
240 cal.vevent.add('url').value = self.url
241 if len(self.lieu) > 0:
242 cal.vevent.add('location').value = self.lieu
74b087e5
EMS
243 if self.piece_jointe:
244 url = self.piece_jointe.url
245 if not url.startswith('http://'):
246 url = SITE_ROOT_URL + url
247 cal.vevent.add('attach').value = url
b7a741ad 248 return cal
249
250 def update_vevent(self,):
251 """Essaie de créer l'évènement sur le serveur ical.
252 En cas de succès, l'évènement local devient donc inactif et approuvé"""
253 try:
254 if self.approuve:
255 event = self.as_ical()
256 client = caldav.DAVClient(CALENDRIER_URL)
257 cal = caldav.Calendar(client, url = CALENDRIER_URL)
258 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
259 e.save()
260 except:
261 self.approuve = False
262
263 def delete_vevent(self,):
264 """Supprime l'evenement sur le serveur caldav"""
265 try:
266 if self.approuve:
267 event = self.as_ical()
268 client = caldav.DAVClient(CALENDRIER_URL)
269 cal = caldav.Calendar(client, url = CALENDRIER_URL)
270 e = cal.event(self.uid)
271 e.delete()
272 except error.NotFoundError:
273 pass
274
264a3210
EMS
275 def assigner_regions(self, regions):
276 self.regions.add(*regions)
277
278 def assigner_disciplines(self, disciplines):
279 if len(disciplines) == 1:
280 if self.discipline:
281 self.discipline_secondaire = disciplines[0]
282 else:
283 self.discipline = disciplines[0]
284 elif len(disciplines) >= 2:
285 self.discipline = disciplines[0]
286 self.discipline_secondaire = disciplines[1]
287
b7a741ad 288
289# Surcharge du comportement de suppression
290# La méthode de connexion par signals est préférable à surcharger la méthode delete()
291# car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
292def delete_vevent(sender, instance, *args, **kwargs):
293 instance.delete_vevent()
294
295pre_delete.connect(delete_vevent, sender = Evenement)
296
297
d972b61d 298class ListSet(models.Model):
299 spec = models.CharField(primary_key = True, max_length = 255)
300 name = models.CharField(max_length = 255)
301 server = models.CharField(max_length = 255)
9eda5d6c 302 validated = models.BooleanField(default = True)
d972b61d 303
10d37e44 304 def __unicode__(self,):
305 return self.name
306
da44ce68
EMS
307class RecordManager(models.Manager):
308
309 def get_query_set(self):
310 return RecordQuerySet(self.model)
311
312 def search(self, text):
313 return self.get_query_set().search(text)
314
f153be1b
EMS
315 def validated(self):
316 return self.get_query_set().validated()
317
15261361 318class RecordQuerySet(models.query.QuerySet, RandomQuerySetMixin):
da44ce68
EMS
319
320 def search(self, text):
f12cc7fb 321 qs = self
da44ce68 322 words = text.split()
da44ce68 323
f12cc7fb
EMS
324 # Ne garder que les ressources qui contiennent tous les mots
325 # demandés.
0b1ddc11 326 q = None
f12cc7fb 327 for word in words:
82f25472 328 matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
0b1ddc11
EMS
329 part = (Q(title__icontains=word) | Q(description__icontains=word) |
330 Q(creator__icontains=word) | Q(contributor__icontains=word) |
331 Q(subject__icontains=word) | Q(disciplines__nom__icontains=word) |
4360ac5f
EMS
332 Q(regions__nom__icontains=word) | Q(pays__in=matching_pays) |
333 Q(publisher__icontains=word))
0b1ddc11
EMS
334 if q is None:
335 q = part
336 else:
337 q = q & part
3c23982e
EMS
338 if q is not None:
339 qs = qs.filter(q).distinct()
f12cc7fb
EMS
340
341 # On donne un point pour chaque mot présent dans le titre.
3c23982e
EMS
342 if words:
343 score_expr = ' + '.join(['(title LIKE %s)'] * len(words))
344 score_params = ['%' + word + '%' for word in words]
345 qs = qs.extra(
346 select={'score': score_expr},
347 select_params=score_params
348 ).order_by('-score')
3c23982e 349 return qs
f12cc7fb
EMS
350
351 def search_auteur(self, text):
352 qs = self
353 for word in text.split():
354 qs = qs.filter(Q(creator__icontains=word) | Q(contributor__icontains=word))
355 return qs
356
357 def search_sujet(self, text):
358 qs = self
359 for word in text.split():
360 qs = qs.filter(subject__icontains=word)
361 return qs
362
363 def search_titre(self, text):
364 qs = self
365 for word in text.split():
366 qs = qs.filter(title__icontains=word)
367 return qs
368
f153be1b
EMS
369 def validated(self):
370 """Ne garder que les ressources validées et qui sont soit dans aucun
371 listset ou au moins dans un listset validé."""
372 qs = self.filter(validated=True)
82f25472
EMS
373 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
374 return qs.distinct()
f153be1b 375
77b0fac0
EMS
376 def filter(self, *args, **kwargs):
377 """Gère des filtres supplémentaires pour l'admin.
378
379 C'est la seule façon que j'ai trouvée de contourner les mécanismes
380 de recherche de l'admin."""
381 search = kwargs.pop('admin_search', None)
382 search_titre = kwargs.pop('admin_search_titre', None)
383 search_sujet = kwargs.pop('admin_search_sujet', None)
384 search_description = kwargs.pop('admin_search_description', None)
385 search_auteur = kwargs.pop('admin_search_auteur', None)
386
387 if search:
388 qs = self
389 search_all = not (search_titre or search_description or search_sujet or search_auteur)
390 fields = []
391 if search_titre or search_all:
392 fields += ['title', 'alt_title']
393 if search_description or search_all:
394 fields += ['description', 'abstract']
395 if search_sujet or search_all:
396 fields += ['subject']
397 if search_auteur or search_all:
398 fields += ['creator', 'contributor']
399
400 for bit in search.split():
401 or_queries = [Q(**{field + '__icontains': bit}) for field in fields]
402 qs = qs.filter(reduce(operator.or_, or_queries))
403
404 if args or kwargs:
405 qs = super(RecordQuerySet, qs).filter(*args, **kwargs)
406 return qs
407 else:
408 return super(RecordQuerySet, self).filter(*args, **kwargs)
409
0cc5f772 410class Record(models.Model):
23b5b3d5 411
412 #fonctionnement interne
0cc5f772 413 id = models.AutoField(primary_key = True)
a5f76eb4 414 server = models.CharField(max_length = 255, verbose_name=u'serveur')
23b5b3d5 415 last_update = models.CharField(max_length = 255)
416 last_checksum = models.CharField(max_length = 255)
a5f76eb4 417 validated = models.BooleanField(default=True, verbose_name=u'validé')
23b5b3d5 418
419 #OAI
18dbd2cf
EMS
420 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
421 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
422 description = models.TextField(null=True, blank=True)
423 modified = models.CharField(max_length=255, null=True, blank=True)
23b5b3d5 424 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
425 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
426 source = models.TextField(null = True, blank = True)
427 contributor = models.TextField(null = True, blank = True)
18dbd2cf 428 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
23b5b3d5 429 publisher = models.TextField(null = True, blank = True)
430 type = models.TextField(null = True, blank = True)
431 format = models.TextField(null = True, blank = True)
432 language = models.TextField(null = True, blank = True)
da9020f3 433
c88d78dc 434 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
d972b61d 435
da9020f3 436 #SEP 2 (aucune données récoltées)
23b5b3d5 437 alt_title = models.TextField(null = True, blank = True)
438 abstract = models.TextField(null = True, blank = True)
439 creation = models.CharField(max_length = 255, null = True, blank = True)
440 issued = models.CharField(max_length = 255, null = True, blank = True)
441 isbn = models.TextField(null = True, blank = True)
442 orig_lang = models.TextField(null = True, blank = True)
da9020f3 443
444 # Metadata AUF multivaluées
a342f93a
EMS
445 disciplines = models.ManyToManyField(Discipline, blank=True)
446 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
447 pays = models.ManyToManyField(Pays, blank=True)
448 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
0cc5f772 449
da44ce68
EMS
450 # Manager
451 objects = RecordManager()
452
18dbd2cf
EMS
453 class Meta:
454 verbose_name = 'ressource'
455
264a3210
EMS
456 def __unicode__(self):
457 return "[%s] %s" % (self.server, self.title)
458
459 def getServeurURL(self):
f98ad449 460 """Retourne l'URL du serveur de provenance"""
461 return RESOURCES[self.server]['url']
462
264a3210 463 def est_complet(self):
6d885e0c 464 """teste si le record à toutes les données obligatoires"""
465 return self.disciplines.count() > 0 and \
466 self.thematiques.count() > 0 and \
467 self.pays.count() > 0 and \
468 self.regions.count() > 0
469
264a3210
EMS
470 def assigner_regions(self, regions):
471 self.regions.add(*regions)
da9020f3 472
264a3210
EMS
473 def assigner_disciplines(self, disciplines):
474 self.disciplines.add(*disciplines)
264a3210 475
6d885e0c 476class Serveur(models.Model):
b7a741ad 477 """Identification d'un serveur d'ou proviennent les références"""
6d885e0c 478 nom = models.CharField(primary_key = True, max_length = 255)
479
480 def __unicode__(self,):
481 return self.nom
482
483 def conf_2_db(self,):
484 for k in RESOURCES.keys():
485 s, created = Serveur.objects.get_or_create(nom=k)
486 s.nom = k
487 s.save()
488
489class Profile(models.Model):
490 user = models.ForeignKey(User, unique=True)
491 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
0cc5f772
CR
492
493class HarvestLog(models.Model):
23b5b3d5 494 context = models.CharField(max_length = 255)
495 name = models.CharField(max_length = 255)
0cc5f772 496 date = models.DateTimeField(auto_now = True)
23b5b3d5 497 added = models.IntegerField(null = True, blank = True)
498 updated = models.IntegerField(null = True, blank = True)
a85ba76e 499 processed = models.IntegerField(null = True, blank = True)
23b5b3d5 500 record = models.ForeignKey(Record, null = True, blank = True)
501
502 @staticmethod
503 def add(message):
504 logger = HarvestLog()
505 if message.has_key('record_id'):
506 message['record'] = Record.objects.get(id=message['record_id'])
507 del(message['record_id'])
508
509 for k,v in message.items():
510 setattr(logger, k, v)
511 logger.save()