e09e6dce34b1576b279e14ac4cd279a7e28f2194
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
1 # -*- encoding: utf-8 -*-
2 import simplejson, uuid, datetime, caldav, vobject, uuid, random, operator
3 from django.contrib.auth.models import User
4 from django.db import models
5 from django.db.models import Q, Max
6 from django.db.models.signals import pre_delete
7 from timezones.fields import TimeZoneField
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
14
15 class 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
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')
30
31 def __unicode__ (self):
32 return self.nom
33
34 class Meta:
35 db_table = u'discipline'
36 ordering = ["nom",]
37
38 class SourceActualite(models.Model):
39 nom = models.CharField(max_length=255)
40 url = models.CharField(max_length=255)
41
42 def __unicode__(self,):
43 return u"%s" % self.nom
44
45 class ActualiteManager(models.Manager):
46
47 def get_query_set(self):
48 return ActualiteQuerySet(self.model)
49
50 def search(self, text):
51 return self.get_query_set().search(text)
52
53 class ActualiteQuerySet(models.query.QuerySet, RandomQuerySetMixin):
54
55 def search(self, text):
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
65
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')
77
78 objects = ActualiteManager()
79
80 class Meta:
81 db_table = u'actualite'
82 ordering = ["-date",]
83
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
93 class 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
101 class EvenementQuerySet(models.query.QuerySet, RandomQuerySetMixin):
102
103 def search(self, text):
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) |
113 Q(contact__icontains=word) |
114 Q(regions__nom__icontains=word))
115 if q is None:
116 q = part
117 else:
118 q = q & part
119 return self.filter(q).distinct() if q is not None else self
120
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
127 class Evenement(models.Model):
128 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
129 (u'Conférence', u'Conférence'),
130 (u'Appel à contribution', u'Appel à contribution'),
131 (u'Journée d\'étude', u'Journée d\'étude'),
132 (None, u'Autre'))
133
134 uid = models.CharField(max_length = 255, default = str(uuid.uuid1()))
135 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
136 titre = models.CharField(max_length=255)
137 discipline = models.ForeignKey('Discipline', related_name = "discipline",
138 blank = True, null = True)
139 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
140 verbose_name=u"discipline secondaire",
141 blank=True, null=True)
142 mots_cles = models.TextField('Mots-Clés', blank = True, null = True)
143 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
144 fuseau = TimeZoneField(verbose_name='fuseau horaire')
145 debut = models.DateTimeField(default = datetime.datetime.now)
146 fin = models.DateTimeField(default = datetime.datetime.now)
147 lieu = models.TextField()
148 description = models.TextField(blank = True, null = True)
149 #fichiers = TODO?
150 contact = models.TextField(blank = True, null = True)
151 url = models.CharField(max_length=255, blank = True, null = True)
152 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
153
154 objects = EvenementManager()
155
156 class Meta:
157 ordering = ['-debut']
158
159 def __unicode__(self,):
160 return "[%s] %s" % (self.uid, self.titre)
161
162 def clean(self):
163 from django.core.exceptions import ValidationError
164 if self.debut > self.fin:
165 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
166
167 def save(self, *args, **kwargs):
168 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
169 approuvé"""
170 self.clean()
171 self.update_vevent()
172 super(Evenement, self).save(*args, **kwargs)
173
174 # methodes de commnunications avec CALDAV
175 def as_ical(self,):
176 """Retourne l'evenement django sous forme d'objet icalendar"""
177 cal = vobject.iCalendar()
178 cal.add('vevent')
179
180 # fournit son propre uid
181 if self.uid in [None, ""]:
182 self.uid = str(uuid.uuid1())
183
184 cal.vevent.add('uid').value = self.uid
185
186 cal.vevent.add('summary').value = self.titre
187
188 if self.mots_cles is None:
189 kw = []
190 else:
191 kw = self.mots_cles.split(",")
192
193 try:
194 kw.append(self.discipline.nom)
195 kw.append(self.discipline_secondaire.nom)
196 kw.append(self.type)
197 except: pass
198
199 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
200 for k in kw:
201 cal.vevent.add('x-auf-keywords').value = k
202
203 description = self.description
204 if len(kw) > 0:
205 if len(self.description) > 0:
206 description += "\n"
207 description += u"Mots-clés: " + ", ".join(kw)
208
209 cal.vevent.add('dtstart').value = combine(self.debut, self.fuseau)
210 cal.vevent.add('dtend').value = combine(self.fin, self.fuseau)
211 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
212 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
213 if len(description) > 0:
214 cal.vevent.add('description').value = description
215 if len(self.contact) > 0:
216 cal.vevent.add('contact').value = self.contact
217 if len(self.url) > 0:
218 cal.vevent.add('url').value = self.url
219 if len(self.lieu) > 0:
220 cal.vevent.add('location').value = self.lieu
221 return cal
222
223 def update_vevent(self,):
224 """Essaie de créer l'évènement sur le serveur ical.
225 En cas de succès, l'évènement local devient donc inactif et approuvé"""
226 try:
227 if self.approuve:
228 event = self.as_ical()
229 client = caldav.DAVClient(CALENDRIER_URL)
230 cal = caldav.Calendar(client, url = CALENDRIER_URL)
231 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
232 e.save()
233 except:
234 self.approuve = False
235
236 def delete_vevent(self,):
237 """Supprime l'evenement sur le serveur caldav"""
238 try:
239 if self.approuve:
240 event = self.as_ical()
241 client = caldav.DAVClient(CALENDRIER_URL)
242 cal = caldav.Calendar(client, url = CALENDRIER_URL)
243 e = cal.event(self.uid)
244 e.delete()
245 except error.NotFoundError:
246 pass
247
248 def assigner_regions(self, regions):
249 self.regions.add(*regions)
250
251 def assigner_disciplines(self, disciplines):
252 if len(disciplines) == 1:
253 if self.discipline:
254 self.discipline_secondaire = disciplines[0]
255 else:
256 self.discipline = disciplines[0]
257 elif len(disciplines) >= 2:
258 self.discipline = disciplines[0]
259 self.discipline_secondaire = disciplines[1]
260
261
262 # Surcharge du comportement de suppression
263 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
264 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
265 def delete_vevent(sender, instance, *args, **kwargs):
266 instance.delete_vevent()
267
268 pre_delete.connect(delete_vevent, sender = Evenement)
269
270
271 class ListSet(models.Model):
272 spec = models.CharField(primary_key = True, max_length = 255)
273 name = models.CharField(max_length = 255)
274 server = models.CharField(max_length = 255)
275 validated = models.BooleanField(default = True)
276
277 def __unicode__(self,):
278 return self.name
279
280 class RecordManager(models.Manager):
281
282 def get_query_set(self):
283 return RecordQuerySet(self.model)
284
285 def search(self, text):
286 return self.get_query_set().search(text)
287
288 def validated(self):
289 return self.get_query_set().validated()
290
291 class RecordQuerySet(models.query.QuerySet, RandomQuerySetMixin):
292
293 def search(self, text):
294 qs = self
295 words = text.split()
296
297 # Ne garder que les ressources qui contiennent tous les mots
298 # demandés.
299 q = None
300 for word in words:
301 matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
302 part = (Q(title__icontains=word) | Q(description__icontains=word) |
303 Q(creator__icontains=word) | Q(contributor__icontains=word) |
304 Q(subject__icontains=word) | Q(disciplines__nom__icontains=word) |
305 Q(regions__nom__icontains=word) | Q(pays__in=matching_pays))
306 if q is None:
307 q = part
308 else:
309 q = q & part
310 if q is not None:
311 qs = qs.filter(q).distinct()
312
313 # On donne un point pour chaque mot présent dans le titre.
314 if words:
315 score_expr = ' + '.join(['(title LIKE %s)'] * len(words))
316 score_params = ['%' + word + '%' for word in words]
317 qs = qs.extra(
318 select={'score': score_expr},
319 select_params=score_params
320 ).order_by('-score')
321 return qs
322
323 def search_auteur(self, text):
324 qs = self
325 for word in text.split():
326 qs = qs.filter(Q(creator__icontains=word) | Q(contributor__icontains=word))
327 return qs
328
329 def search_sujet(self, text):
330 qs = self
331 for word in text.split():
332 qs = qs.filter(subject__icontains=word)
333 return qs
334
335 def search_titre(self, text):
336 qs = self
337 for word in text.split():
338 qs = qs.filter(title__icontains=word)
339 return qs
340
341 def validated(self):
342 """Ne garder que les ressources validées et qui sont soit dans aucun
343 listset ou au moins dans un listset validé."""
344 qs = self.filter(validated=True)
345 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
346 return qs.distinct()
347
348 def filter(self, *args, **kwargs):
349 """Gère des filtres supplémentaires pour l'admin.
350
351 C'est la seule façon que j'ai trouvée de contourner les mécanismes
352 de recherche de l'admin."""
353 search = kwargs.pop('admin_search', None)
354 search_titre = kwargs.pop('admin_search_titre', None)
355 search_sujet = kwargs.pop('admin_search_sujet', None)
356 search_description = kwargs.pop('admin_search_description', None)
357 search_auteur = kwargs.pop('admin_search_auteur', None)
358
359 if search:
360 qs = self
361 search_all = not (search_titre or search_description or search_sujet or search_auteur)
362 fields = []
363 if search_titre or search_all:
364 fields += ['title', 'alt_title']
365 if search_description or search_all:
366 fields += ['description', 'abstract']
367 if search_sujet or search_all:
368 fields += ['subject']
369 if search_auteur or search_all:
370 fields += ['creator', 'contributor']
371
372 for bit in search.split():
373 or_queries = [Q(**{field + '__icontains': bit}) for field in fields]
374 qs = qs.filter(reduce(operator.or_, or_queries))
375
376 if args or kwargs:
377 qs = super(RecordQuerySet, qs).filter(*args, **kwargs)
378 return qs
379 else:
380 return super(RecordQuerySet, self).filter(*args, **kwargs)
381
382
383
384 class Record(models.Model):
385
386 #fonctionnement interne
387 id = models.AutoField(primary_key = True)
388 server = models.CharField(max_length = 255, verbose_name=u'serveur')
389 last_update = models.CharField(max_length = 255)
390 last_checksum = models.CharField(max_length = 255)
391 validated = models.BooleanField(default=True, verbose_name=u'validé')
392
393 #OAI
394 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
395 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
396 description = models.TextField(null=True, blank=True)
397 modified = models.CharField(max_length=255, null=True, blank=True)
398 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
399 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
400 source = models.TextField(null = True, blank = True)
401 contributor = models.TextField(null = True, blank = True)
402 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
403 publisher = models.TextField(null = True, blank = True)
404 type = models.TextField(null = True, blank = True)
405 format = models.TextField(null = True, blank = True)
406 language = models.TextField(null = True, blank = True)
407
408 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
409
410 #SEP 2 (aucune données récoltées)
411 alt_title = models.TextField(null = True, blank = True)
412 abstract = models.TextField(null = True, blank = True)
413 creation = models.CharField(max_length = 255, null = True, blank = True)
414 issued = models.CharField(max_length = 255, null = True, blank = True)
415 isbn = models.TextField(null = True, blank = True)
416 orig_lang = models.TextField(null = True, blank = True)
417
418 # Metadata AUF multivaluées
419 disciplines = models.ManyToManyField(Discipline, blank=True)
420 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
421 pays = models.ManyToManyField(Pays, blank=True)
422 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
423
424 # Manager
425 objects = RecordManager()
426
427 class Meta:
428 verbose_name = 'ressource'
429
430 def __unicode__(self):
431 return "[%s] %s" % (self.server, self.title)
432
433 def getServeurURL(self):
434 """Retourne l'URL du serveur de provenance"""
435 return RESOURCES[self.server]['url']
436
437 def est_complet(self):
438 """teste si le record à toutes les données obligatoires"""
439 return self.disciplines.count() > 0 and \
440 self.thematiques.count() > 0 and \
441 self.pays.count() > 0 and \
442 self.regions.count() > 0
443
444 def assigner_regions(self, regions):
445 self.regions.add(*regions)
446
447 def assigner_disciplines(self, disciplines):
448 self.disciplines.add(*disciplines)
449
450
451 class Serveur(models.Model):
452 """Identification d'un serveur d'ou proviennent les références"""
453 nom = models.CharField(primary_key = True, max_length = 255)
454
455 def __unicode__(self,):
456 return self.nom
457
458 def conf_2_db(self,):
459 for k in RESOURCES.keys():
460 s, created = Serveur.objects.get_or_create(nom=k)
461 s.nom = k
462 s.save()
463
464 class Profile(models.Model):
465 user = models.ForeignKey(User, unique=True)
466 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
467
468 class HarvestLog(models.Model):
469 context = models.CharField(max_length = 255)
470 name = models.CharField(max_length = 255)
471 date = models.DateTimeField(auto_now = True)
472 added = models.IntegerField(null = True, blank = True)
473 updated = models.IntegerField(null = True, blank = True)
474 processed = models.IntegerField(null = True, blank = True)
475 record = models.ForeignKey(Record, null = True, blank = True)
476
477 @staticmethod
478 def add(message):
479 logger = HarvestLog()
480 if message.has_key('record_id'):
481 message['record'] = Record.objects.get(id=message['record_id'])
482 del(message['record_id'])
483
484 for k,v in message.items():
485 setattr(logger, k, v)
486 logger.save()