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