Tronquer les titres d'établissements dans le formulaire du chercheur.
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
CommitLineData
92c7413b 1# -*- encoding: utf-8 -*-
77b0fac0 2import simplejson, uuid, datetime, caldav, vobject, uuid, random, operator
6d885e0c 3from django.contrib.auth.models import User
d15017b2 4from django.db import models
15261361 5from django.db.models import Q, Max
b7a741ad 6from django.db.models.signals import pre_delete
92c7413b 7from timezones.fields import TimeZoneField
6d885e0c 8from auf_savoirs_en_partage.backend_config import RESOURCES
da9020f3 9from savoirs.globals import META
b7a741ad 10from settings import CALENDRIER_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
92c7413b 127class Evenement(models.Model):
7bbf600c
EMS
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
c5b3da8b 134 uid = models.CharField(max_length = 255, default = str(uuid.uuid1()))
a5f76eb4 135 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
92c7413b
CR
136 titre = models.CharField(max_length=255)
137 discipline = models.ForeignKey('Discipline', related_name = "discipline",
138 blank = True, null = True)
a5f76eb4
EMS
139 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
140 verbose_name=u"discipline secondaire",
141 blank=True, null=True)
92c7413b 142 mots_cles = models.TextField('Mots-Clés', blank = True, null = True)
7bbf600c 143 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
a5f76eb4 144 fuseau = TimeZoneField(verbose_name='fuseau horaire')
92c7413b
CR
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)
a5f76eb4 152 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
92c7413b 153
4101cfc0
EMS
154 objects = EvenementManager()
155
156 class Meta:
157 ordering = ['-debut']
158
020f79a9 159 def __unicode__(self,):
160 return "[%s] %s" % (self.uid, self.titre)
161
73309469 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
b7a741ad 167 def save(self, *args, **kwargs):
168 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
169 approuvé"""
73309469 170 self.clean()
b7a741ad 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
7f56d0d4 181 if self.uid in [None, ""]:
b7a741ad 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
79b400f0 199 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
b7a741ad 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"
028f548f 207 description += u"Mots-clés: " + ", ".join(kw)
b7a741ad 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")
79b400f0 213 if len(description) > 0:
b7a741ad 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
264a3210
EMS
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
b7a741ad 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
265def delete_vevent(sender, instance, *args, **kwargs):
266 instance.delete_vevent()
267
268pre_delete.connect(delete_vevent, sender = Evenement)
269
270
d972b61d 271class 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)
9eda5d6c 275 validated = models.BooleanField(default = True)
d972b61d 276
10d37e44 277 def __unicode__(self,):
278 return self.name
279
da44ce68
EMS
280class 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
f153be1b
EMS
288 def validated(self):
289 return self.get_query_set().validated()
290
15261361 291class RecordQuerySet(models.query.QuerySet, RandomQuerySetMixin):
da44ce68
EMS
292
293 def search(self, text):
f12cc7fb 294 qs = self
da44ce68 295 words = text.split()
da44ce68 296
f12cc7fb
EMS
297 # Ne garder que les ressources qui contiennent tous les mots
298 # demandés.
0b1ddc11 299 q = None
f12cc7fb 300 for word in words:
82f25472 301 matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
0b1ddc11
EMS
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) |
82f25472 305 Q(regions__nom__icontains=word) | Q(pays__in=matching_pays))
0b1ddc11
EMS
306 if q is None:
307 q = part
308 else:
309 q = q & part
3c23982e
EMS
310 if q is not None:
311 qs = qs.filter(q).distinct()
f12cc7fb
EMS
312
313 # On donne un point pour chaque mot présent dans le titre.
3c23982e
EMS
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')
3c23982e 321 return qs
f12cc7fb
EMS
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
f153be1b
EMS
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)
82f25472
EMS
345 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
346 return qs.distinct()
f153be1b 347
77b0fac0
EMS
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
0cc5f772 384class Record(models.Model):
23b5b3d5 385
386 #fonctionnement interne
0cc5f772 387 id = models.AutoField(primary_key = True)
a5f76eb4 388 server = models.CharField(max_length = 255, verbose_name=u'serveur')
23b5b3d5 389 last_update = models.CharField(max_length = 255)
390 last_checksum = models.CharField(max_length = 255)
a5f76eb4 391 validated = models.BooleanField(default=True, verbose_name=u'validé')
23b5b3d5 392
393 #OAI
18dbd2cf
EMS
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)
23b5b3d5 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)
18dbd2cf 402 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
23b5b3d5 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)
da9020f3 407
c88d78dc 408 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
d972b61d 409
da9020f3 410 #SEP 2 (aucune données récoltées)
23b5b3d5 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)
da9020f3 417
418 # Metadata AUF multivaluées
a342f93a
EMS
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')
0cc5f772 423
da44ce68
EMS
424 # Manager
425 objects = RecordManager()
426
18dbd2cf
EMS
427 class Meta:
428 verbose_name = 'ressource'
429
264a3210
EMS
430 def __unicode__(self):
431 return "[%s] %s" % (self.server, self.title)
432
433 def getServeurURL(self):
f98ad449 434 """Retourne l'URL du serveur de provenance"""
435 return RESOURCES[self.server]['url']
436
264a3210 437 def est_complet(self):
6d885e0c 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
264a3210
EMS
444 def assigner_regions(self, regions):
445 self.regions.add(*regions)
da9020f3 446
264a3210
EMS
447 def assigner_disciplines(self, disciplines):
448 self.disciplines.add(*disciplines)
449
450
6d885e0c 451class Serveur(models.Model):
b7a741ad 452 """Identification d'un serveur d'ou proviennent les références"""
6d885e0c 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
464class Profile(models.Model):
465 user = models.ForeignKey(User, unique=True)
466 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
0cc5f772
CR
467
468class HarvestLog(models.Model):
23b5b3d5 469 context = models.CharField(max_length = 255)
470 name = models.CharField(max_length = 255)
0cc5f772 471 date = models.DateTimeField(auto_now = True)
23b5b3d5 472 added = models.IntegerField(null = True, blank = True)
473 updated = models.IntegerField(null = True, blank = True)
a85ba76e 474 processed = models.IntegerField(null = True, blank = True)
23b5b3d5 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()