b7b803191f6b1ba4d4b4c507ca71824ec629aff1
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
1 # -*- encoding: utf-8 -*-
2 import caldav
3 import datetime
4 import feedparser
5 import operator
6 import os
7 import pytz
8 import random
9 import uuid
10 import vobject
11 from backend_config import RESOURCES
12 from babel.dates import get_timezone_name
13 from caldav.lib import error
14 from babel.dates import get_timezone_name
15 from datamaster_modeles.models import Region, Pays, Thematique
16 from django.contrib.auth.models import User
17 from django.db import models
18 from django.db.models import Q, Max
19 from django.db.models.signals import pre_delete
20 from django.utils.encoding import smart_unicode
21 from djangosphinx.models import SphinxQuerySet
22 from savoirs.globals import META
23 from settings import CALENDRIER_URL, SITE_ROOT_URL
24
25 # Fonctionnalités communes à tous les query sets
26
27 class RandomQuerySetMixin(object):
28 """Mixin pour les modèles.
29
30 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
31 méthode pour récupérer des objets au hasard.
32 """
33
34 def random(self, n=1):
35 """Récupère aléatoirement un nombre donné d'objets."""
36 count = self.count()
37 positions = random.sample(xrange(count), min(n, count))
38 return [self[p] for p in positions]
39
40 class SEPQuerySet(models.query.QuerySet, RandomQuerySetMixin):
41 pass
42
43 class SEPSphinxQuerySet(SphinxQuerySet, RandomQuerySetMixin):
44 """Fonctionnalités communes aux query sets de Sphinx."""
45
46 def __init__(self, model=None, index=None, weights=None):
47 SphinxQuerySet.__init__(self, model=model, index=index,
48 mode='SPH_MATCH_EXTENDED2',
49 rankmode='SPH_RANK_PROXIMITY_BM25',
50 weights=weights)
51
52 def add_to_query(self, query):
53 """Ajoute une partie à la requête texte."""
54
55 # Assurons-nous qu'il y a un nombre pair de guillemets
56 if query.count('"') % 2 != 0:
57 # Sinon, on enlève le dernier (faut choisir...)
58 i = query.rindex('"')
59 query = query[:i] + query[i+1:]
60
61 new_query = smart_unicode(self._query) + ' ' + query if self._query else query
62 return self.query(new_query)
63
64 def search(self, text):
65 """Recherche ``text`` dans tous les champs."""
66 return self.add_to_query('@* ' + text)
67
68 def filter_discipline(self, discipline):
69 """Par défaut, le filtre par discipline cherche le nom de la
70 discipline dans tous les champs."""
71 return self.search('"%s"' % discipline.nom)
72
73 def filter_region(self, region):
74 """Par défaut, le filtre par région cherche le nom de la région dans
75 tous les champs."""
76 return self.search('"%s"' % region.nom)
77
78 class SEPManager(models.Manager):
79 """Lorsque les méthodes ``search``, ``filter_region`` et
80 ``filter_discipline`` sont appelées sur ce manager, le query set
81 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
82
83 def query(self, query):
84 return self.get_sphinx_query_set().query(query)
85
86 def add_to_query(self, query):
87 return self.get_sphinx_query_set().add_to_query(query)
88
89 def search(self, text):
90 return self.get_sphinx_query_set().search(text)
91
92 def filter_region(self, region):
93 return self.get_sphinx_query_set().filter_region(region)
94
95 def filter_discipline(self, discipline):
96 return self.get_sphinx_query_set().filter_discipline(discipline)
97
98 # Disciplines
99
100 class Discipline(models.Model):
101 id = models.IntegerField(primary_key=True, db_column='id_discipline')
102 nom = models.CharField(max_length=765, db_column='nom_discipline')
103
104 def __unicode__ (self):
105 return self.nom
106
107 class Meta:
108 db_table = u'discipline'
109 ordering = ["nom",]
110
111 # Actualités
112
113 class SourceActualite(models.Model):
114 nom = models.CharField(max_length=255)
115 url = models.CharField(max_length=255, verbose_name='URL')
116
117 class Meta:
118 verbose_name = u'fil RSS syndiqué'
119 verbose_name_plural = u'fils RSS syndiqués'
120
121 def __unicode__(self,):
122 return u"%s" % self.nom
123
124 def update(self):
125 """Mise à jour du fil RSS."""
126 feed = feedparser.parse(self.url)
127 for entry in feed.entries:
128 if Actualite.all_objects.filter(url=entry.link).count() == 0:
129 ts = entry.updated_parsed
130 date = datetime.date(ts.tm_year, ts.tm_mon, ts.tm_mday)
131 a = self.actualites.create(titre=entry.title,
132 texte=entry.summary_detail.value,
133 url=entry.link, date=date)
134
135 class ActualiteQuerySet(SEPQuerySet):
136
137 def filter_date(self, min=None, max=None):
138 qs = self
139 if min:
140 qs = qs.filter(date__gte=min)
141 if max:
142 qs = qs.filter(date__lte=max)
143 return qs
144
145 class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
146
147 def __init__(self, model=None):
148 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_actualites',
149 weights=dict(titre=3))
150
151 def filter_date(self, min=None, max=None):
152 qs = self
153 if min:
154 qs = qs.filter(date__gte=min.toordinal()+365)
155 if max:
156 qs = qs.filter(date__lte=max.toordinal()+365)
157 return qs
158
159 class ActualiteManager(SEPManager):
160
161 def get_query_set(self):
162 return ActualiteQuerySet(self.model).filter(visible=True)
163
164 def get_sphinx_query_set(self):
165 return ActualiteSphinxQuerySet(self.model).order_by('-date')
166
167 def filter_date(self, min=None, max=None):
168 return self.get_query_set().filter_date(min=min, max=max)
169
170 class Actualite(models.Model):
171 id = models.AutoField(primary_key=True, db_column='id_actualite')
172 titre = models.CharField(max_length=765, db_column='titre_actualite')
173 texte = models.TextField(db_column='texte_actualite')
174 url = models.CharField(max_length=765, db_column='url_actualite')
175 date = models.DateField(db_column='date_actualite')
176 visible = models.BooleanField(db_column='visible_actualite', default=False)
177 ancienid = models.IntegerField(db_column='ancienId_actualite', blank=True, null=True)
178 source = models.ForeignKey(SourceActualite, blank=True, null=True, related_name='actualites')
179 disciplines = models.ManyToManyField(Discipline, blank=True, related_name="actualites")
180 regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
181
182 objects = ActualiteManager()
183 all_objects = models.Manager()
184
185 class Meta:
186 db_table = u'actualite'
187 ordering = ["-date"]
188
189 def __unicode__ (self):
190 return "%s" % (self.titre)
191
192 def assigner_disciplines(self, disciplines):
193 self.disciplines.add(*disciplines)
194
195 def assigner_regions(self, regions):
196 self.regions.add(*regions)
197
198 # Agenda
199
200 class EvenementQuerySet(SEPQuerySet):
201
202 def filter_type(self, type):
203 return self.filter(type=type)
204
205 def filter_debut(self, min=None, max=None):
206 qs = self
207 if min:
208 qs = qs.filter(debut__gte=min)
209 if max:
210 qs = qs.filter(debut__lt=max+datetime.timedelta(days=1))
211 return qs
212
213 class EvenementSphinxQuerySet(SEPSphinxQuerySet):
214
215 def __init__(self, model=None):
216 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_evenements',
217 weights=dict(titre=3))
218
219 def filter_type(self, type):
220 return self.add_to_query('@type "%s"' % type)
221
222 def filter_debut(self, min=None, max=None):
223 qs = self
224 if min:
225 qs = qs.filter(debut__gte=min.toordinal()+365)
226 if max:
227 qs = qs.filter(debut__lte=max.toordinal()+365)
228 return qs
229
230 class EvenementManager(SEPManager):
231
232 def get_query_set(self):
233 return EvenementQuerySet(self.model).filter(approuve=True)
234
235 def get_sphinx_query_set(self):
236 return EvenementSphinxQuerySet(self.model).order_by('-debut')
237
238 def filter_type(self, type):
239 return self.get_query_set().filter_type(type)
240
241 def filter_debut(self, min=None, max=None):
242 return self.get_query_set().filter_debut(min=min, max=max)
243
244 def build_time_zone_choices(pays=None):
245 timezones = pytz.country_timezones[pays] if pays else pytz.common_timezones
246 result = []
247 now = datetime.datetime.now()
248 for tzname in timezones:
249 tz = pytz.timezone(tzname)
250 fr_name = get_timezone_name(tz, locale='fr_FR')
251 offset = tz.utcoffset(now)
252 seconds = offset.seconds + offset.days * 86400
253 (hours, minutes) = divmod(seconds // 60, 60)
254 offset_str = 'UTC%+d:%d' % (hours, minutes) if minutes else 'UTC%+d' % hours
255 result.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
256 result.sort()
257 return [(x[1], x[2]) for x in result]
258
259 class Evenement(models.Model):
260 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
261 (u'Conférence', u'Conférence'),
262 (u'Appel à contribution', u'Appel à contribution'),
263 (u'Journée d\'étude', u'Journée d\'étude'),
264 (u'None', u'Autre'))
265 TIME_ZONE_CHOICES = build_time_zone_choices()
266
267 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
268 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
269 titre = models.CharField(max_length=255)
270 discipline = models.ForeignKey('Discipline', related_name = "discipline",
271 blank = True, null = True)
272 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
273 verbose_name=u"discipline secondaire",
274 blank=True, null=True)
275 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
276 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
277 lieu = models.TextField()
278 debut = models.DateTimeField(default=datetime.datetime.now)
279 fin = models.DateTimeField(default=datetime.datetime.now)
280 pays = models.ForeignKey(Pays, related_name='evenements', null=True, blank=True)
281 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
282 description = models.TextField(blank=True, null=True)
283 contact = models.TextField(blank=True, null=True)
284 url = models.CharField(max_length=255, blank=True, null=True)
285 piece_jointe = models.FileField(upload_to='agenda/pj', blank=True, verbose_name='pièce jointe')
286 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
287
288 objects = EvenementManager()
289 all_objects = models.Manager()
290
291 class Meta:
292 ordering = ['-debut']
293
294 def __unicode__(self,):
295 return "[%s] %s" % (self.uid, self.titre)
296
297 def duration_display(self):
298 delta = self.fin - self.debut
299 minutes, seconds = divmod(delta.seconds, 60)
300 hours, minutes = divmod(minutes, 60)
301 days = delta.days
302 parts = []
303 if days == 1:
304 parts.append('1 jour')
305 elif days > 1:
306 parts.append('%d jours' % days)
307 if hours == 1:
308 parts.append('1 heure')
309 elif hours > 1:
310 parts.append('%d heures' % hours)
311 if minutes == 1:
312 parts.append('1 minute')
313 elif minutes > 1:
314 parts.append('%d minutes' % minutes)
315 return ' '.join(parts)
316
317 def piece_jointe_display(self):
318 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
319
320 def clean(self):
321 from django.core.exceptions import ValidationError
322 if self.debut > self.fin:
323 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
324
325 def save(self, *args, **kwargs):
326 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
327 approuvé"""
328 self.clean()
329 super(Evenement, self).save(*args, **kwargs)
330 self.update_vevent()
331
332 # methodes de commnunications avec CALDAV
333 def as_ical(self,):
334 """Retourne l'evenement django sous forme d'objet icalendar"""
335 cal = vobject.iCalendar()
336 cal.add('vevent')
337
338 # fournit son propre uid
339 if self.uid in [None, ""]:
340 self.uid = str(uuid.uuid1())
341
342 cal.vevent.add('uid').value = self.uid
343
344 cal.vevent.add('summary').value = self.titre
345
346 if self.mots_cles is None:
347 kw = []
348 else:
349 kw = self.mots_cles.split(",")
350
351 try:
352 kw.append(self.discipline.nom)
353 kw.append(self.discipline_secondaire.nom)
354 kw.append(self.type)
355 except: pass
356
357 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
358 for k in kw:
359 cal.vevent.add('x-auf-keywords').value = k
360
361 description = self.description
362 if len(kw) > 0:
363 if len(self.description) > 0:
364 description += "\n"
365 description += u"Mots-clés: " + ", ".join(kw)
366
367 cal.vevent.add('dtstart').value = combine(self.debut, pytz.timezone(self.fuseau))
368 cal.vevent.add('dtend').value = combine(self.fin, pytz.timezone(self.fuseau))
369 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
370 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
371 if len(description) > 0:
372 cal.vevent.add('description').value = description
373 if len(self.contact) > 0:
374 cal.vevent.add('contact').value = self.contact
375 if len(self.url) > 0:
376 cal.vevent.add('url').value = self.url
377 if len(self.lieu) > 0:
378 cal.vevent.add('location').value = self.lieu
379 if self.piece_jointe:
380 url = self.piece_jointe.url
381 if not url.startswith('http://'):
382 url = SITE_ROOT_URL + url
383 cal.vevent.add('attach').value = url
384 return cal
385
386 def update_vevent(self,):
387 """Essaie de créer l'évènement sur le serveur ical.
388 En cas de succès, l'évènement local devient donc inactif et approuvé"""
389 try:
390 if self.approuve:
391 event = self.as_ical()
392 client = caldav.DAVClient(CALENDRIER_URL)
393 cal = caldav.Calendar(client, url = CALENDRIER_URL)
394 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
395 e.save()
396 except:
397 self.approuve = False
398
399 def delete_vevent(self,):
400 """Supprime l'evenement sur le serveur caldav"""
401 try:
402 if self.approuve:
403 event = self.as_ical()
404 client = caldav.DAVClient(CALENDRIER_URL)
405 cal = caldav.Calendar(client, url = CALENDRIER_URL)
406 e = cal.event(self.uid)
407 e.delete()
408 except error.NotFoundError:
409 pass
410
411 def assigner_regions(self, regions):
412 self.regions.add(*regions)
413
414 def assigner_disciplines(self, disciplines):
415 if len(disciplines) == 1:
416 if self.discipline:
417 self.discipline_secondaire = disciplines[0]
418 else:
419 self.discipline = disciplines[0]
420 elif len(disciplines) >= 2:
421 self.discipline = disciplines[0]
422 self.discipline_secondaire = disciplines[1]
423
424 def delete_vevent(sender, instance, *args, **kwargs):
425 # Surcharge du comportement de suppression
426 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
427 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
428 instance.delete_vevent()
429 pre_delete.connect(delete_vevent, sender=Evenement)
430
431 # Ressources
432
433 class ListSet(models.Model):
434 spec = models.CharField(primary_key = True, max_length = 255)
435 name = models.CharField(max_length = 255)
436 server = models.CharField(max_length = 255)
437 validated = models.BooleanField(default = True)
438
439 def __unicode__(self,):
440 return self.name
441
442 class RecordSphinxQuerySet(SEPSphinxQuerySet):
443
444 def __init__(self, model=None):
445 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_ressources',
446 weights=dict(title=3))
447
448 class RecordManager(SEPManager):
449
450 def get_query_set(self):
451 """Ne garder que les ressources validées et qui sont soit dans aucun
452 listset ou au moins dans un listset validé."""
453 qs = SEPQuerySet(self.model)
454 qs = qs.filter(validated=True)
455 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
456 return qs.distinct()
457
458 def get_sphinx_query_set(self):
459 return RecordSphinxQuerySet(self.model)
460
461 class Record(models.Model):
462
463 #fonctionnement interne
464 id = models.AutoField(primary_key = True)
465 server = models.CharField(max_length = 255, verbose_name=u'serveur')
466 last_update = models.CharField(max_length = 255)
467 last_checksum = models.CharField(max_length = 255)
468 validated = models.BooleanField(default=True, verbose_name=u'validé')
469
470 #OAI
471 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
472 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
473 description = models.TextField(null=True, blank=True)
474 modified = models.CharField(max_length=255, null=True, blank=True)
475 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
476 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
477 source = models.TextField(null = True, blank = True)
478 contributor = models.TextField(null = True, blank = True)
479 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
480 publisher = models.TextField(null = True, blank = True)
481 type = models.TextField(null = True, blank = True)
482 format = models.TextField(null = True, blank = True)
483 language = models.TextField(null = True, blank = True)
484
485 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
486
487 #SEP 2 (aucune données récoltées)
488 alt_title = models.TextField(null = True, blank = True)
489 abstract = models.TextField(null = True, blank = True)
490 creation = models.CharField(max_length = 255, null = True, blank = True)
491 issued = models.CharField(max_length = 255, null = True, blank = True)
492 isbn = models.TextField(null = True, blank = True)
493 orig_lang = models.TextField(null = True, blank = True)
494
495 # Metadata AUF multivaluées
496 disciplines = models.ManyToManyField(Discipline, blank=True)
497 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
498 pays = models.ManyToManyField(Pays, blank=True)
499 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
500
501 # Managers
502 objects = RecordManager()
503 all_objects = models.Manager()
504
505 class Meta:
506 verbose_name = 'ressource'
507
508 def __unicode__(self):
509 return "[%s] %s" % (self.server, self.title)
510
511 def getServeurURL(self):
512 """Retourne l'URL du serveur de provenance"""
513 return RESOURCES[self.server]['url']
514
515 def est_complet(self):
516 """teste si le record à toutes les données obligatoires"""
517 return self.disciplines.count() > 0 and \
518 self.thematiques.count() > 0 and \
519 self.pays.count() > 0 and \
520 self.regions.count() > 0
521
522 def assigner_regions(self, regions):
523 self.regions.add(*regions)
524
525 def assigner_disciplines(self, disciplines):
526 self.disciplines.add(*disciplines)
527
528 class Serveur(models.Model):
529 """Identification d'un serveur d'ou proviennent les références"""
530 nom = models.CharField(primary_key = True, max_length = 255)
531
532 def __unicode__(self,):
533 return self.nom
534
535 def conf_2_db(self,):
536 for k in RESOURCES.keys():
537 s, created = Serveur.objects.get_or_create(nom=k)
538 s.nom = k
539 s.save()
540
541 class Profile(models.Model):
542 user = models.ForeignKey(User, unique=True)
543 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
544
545 class HarvestLog(models.Model):
546 context = models.CharField(max_length = 255)
547 name = models.CharField(max_length = 255)
548 date = models.DateTimeField(auto_now = True)
549 added = models.IntegerField(null = True, blank = True)
550 updated = models.IntegerField(null = True, blank = True)
551 processed = models.IntegerField(null = True, blank = True)
552 record = models.ForeignKey(Record, null = True, blank = True)
553
554 @staticmethod
555 def add(message):
556 logger = HarvestLog()
557 if message.has_key('record_id'):
558 message['record'] = Record.all_objects.get(id=message['record_id'])
559 del(message['record_id'])
560
561 for k,v in message.items():
562 setattr(logger, k, v)
563 logger.save()