Bugfix: #1308
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
CommitLineData
92c7413b 1# -*- encoding: utf-8 -*-
db2999fa
EMS
2import caldav
3import datetime
4import feedparser
5import operator
6import os
7import pytz
8import random
9import uuid
10import vobject
5ffde8a4 11from pytz.tzinfo import AmbiguousTimeError, NonExistentTimeError
fdcf5874
EMS
12from urllib import urlencode
13
5212238e 14from backend_config import RESOURCES
86983865 15from babel.dates import get_timezone_name
5212238e 16from caldav.lib import error
6d885e0c 17from django.contrib.auth.models import User
fdcf5874
EMS
18from django.contrib.contenttypes.models import ContentType
19from django.core.urlresolvers import reverse
d15017b2 20from django.db import models
15261361 21from django.db.models import Q, Max
b7a741ad 22from django.db.models.signals import pre_delete
5212238e 23from django.utils.encoding import smart_unicode
122c4c3d 24from djangosphinx.models import SphinxQuerySet, SearchError
fdcf5874
EMS
25
26from datamaster_modeles.models import Region, Pays, Thematique
da9020f3 27from savoirs.globals import META
74b087e5 28from settings import CALENDRIER_URL, SITE_ROOT_URL
5212238e
EMS
29
30# Fonctionnalités communes à tous les query sets
d15017b2 31
15261361
EMS
32class RandomQuerySetMixin(object):
33 """Mixin pour les modèles.
34
35 ORDER BY RAND() est très lent sous MySQL. On a besoin d'une autre
36 méthode pour récupérer des objets au hasard.
37 """
38
39 def random(self, n=1):
40 """Récupère aléatoirement un nombre donné d'objets."""
bae03b7b
EMS
41 count = self.count()
42 positions = random.sample(xrange(count), min(n, count))
43 return [self[p] for p in positions]
15261361 44
5212238e 45class SEPQuerySet(models.query.QuerySet, RandomQuerySetMixin):
230671ff
EMS
46
47 def _filter_date(self, field, min=None, max=None):
48 """Limite les résultats à ceux dont le champ ``field`` tombe entre
49 les dates ``min`` et ``max``."""
50 qs = self
51 if min:
52 qs = qs.filter(**{field + '__gte': min})
53 if max:
54 qs = qs.filter(**{field + '__lte': max})
55 return qs
5212238e
EMS
56
57class SEPSphinxQuerySet(SphinxQuerySet, RandomQuerySetMixin):
58 """Fonctionnalités communes aux query sets de Sphinx."""
59
60 def __init__(self, model=None, index=None, weights=None):
61 SphinxQuerySet.__init__(self, model=model, index=index,
62 mode='SPH_MATCH_EXTENDED2',
63 rankmode='SPH_RANK_PROXIMITY_BM25',
64 weights=weights)
65
66 def add_to_query(self, query):
67 """Ajoute une partie à la requête texte."""
6c78c673
EMS
68
69 # Assurons-nous qu'il y a un nombre pair de guillemets
70 if query.count('"') % 2 != 0:
71 # Sinon, on enlève le dernier (faut choisir...)
72 i = query.rindex('"')
73 query = query[:i] + query[i+1:]
74
5212238e
EMS
75 new_query = smart_unicode(self._query) + ' ' + query if self._query else query
76 return self.query(new_query)
77
78 def search(self, text):
79 """Recherche ``text`` dans tous les champs."""
80 return self.add_to_query('@* ' + text)
81
82 def filter_discipline(self, discipline):
83 """Par défaut, le filtre par discipline cherche le nom de la
84 discipline dans tous les champs."""
85 return self.search('"%s"' % discipline.nom)
86
87 def filter_region(self, region):
88 """Par défaut, le filtre par région cherche le nom de la région dans
89 tous les champs."""
90 return self.search('"%s"' % region.nom)
91
230671ff
EMS
92 def _filter_date(self, field, min=None, max=None):
93 """Limite les résultats à ceux dont le champ ``field`` tombe entre
94 les dates ``min`` et ``max``."""
95 qs = self
96 if min:
97 qs = qs.filter(**{field + '__gte': min.toordinal()+365})
98 if max:
99 qs = qs.filter(**{field + '__lte': max.toordinal()+365})
100 return qs
101
122c4c3d
EMS
102 def _get_sphinx_results(self):
103 try:
104 return SphinxQuerySet._get_sphinx_results(self)
105 except SearchError:
106 # Essayons d'enlever les caractères qui peuvent poser problème.
107 for c in '|!@()~/<=^$':
108 self._query = self._query.replace(c, ' ')
109 try:
110 return SphinxQuerySet._get_sphinx_results(self)
111 except SearchError:
112 # Ça ne marche toujours pas. Enlevons les guillemets et les
113 # tirets.
114 for c in '"-':
115 self._query = self._query.replace(c, ' ')
116 return SphinxQuerySet._get_sphinx_results(self)
117
5212238e
EMS
118class SEPManager(models.Manager):
119 """Lorsque les méthodes ``search``, ``filter_region`` et
120 ``filter_discipline`` sont appelées sur ce manager, le query set
121 Sphinx est créé, sinon, c'est le query set Django qui est créé."""
122
123 def query(self, query):
124 return self.get_sphinx_query_set().query(query)
125
126 def add_to_query(self, query):
127 return self.get_sphinx_query_set().add_to_query(query)
128
129 def search(self, text):
130 return self.get_sphinx_query_set().search(text)
131
132 def filter_region(self, region):
133 return self.get_sphinx_query_set().filter_region(region)
134
135 def filter_discipline(self, discipline):
136 return self.get_sphinx_query_set().filter_discipline(discipline)
137
138# Disciplines
139
d15017b2
CR
140class Discipline(models.Model):
141 id = models.IntegerField(primary_key=True, db_column='id_discipline')
142 nom = models.CharField(max_length=765, db_column='nom_discipline')
6ef8ead4
CR
143
144 def __unicode__ (self):
92c7413b 145 return self.nom
6ef8ead4 146
d15017b2
CR
147 class Meta:
148 db_table = u'discipline'
149 ordering = ["nom",]
150
5212238e
EMS
151# Actualités
152
79c398f6 153class SourceActualite(models.Model):
011804bb
EMS
154 TYPE_CHOICES = (
155 ('actu', 'Actualités'),
156 ('appels', "Appels d'offres"),
157 )
158
79c398f6 159 nom = models.CharField(max_length=255)
011804bb
EMS
160 url = models.CharField(max_length=255, verbose_name='URL', blank=True)
161 type = models.CharField(max_length=10, default='actu', choices=TYPE_CHOICES)
ccbc4363 162
db2999fa
EMS
163 class Meta:
164 verbose_name = u'fil RSS syndiqué'
165 verbose_name_plural = u'fils RSS syndiqués'
166
ccbc4363 167 def __unicode__(self,):
011804bb 168 return u"%s (%s)" % (self.nom, self.get_type_display())
79c398f6 169
db2999fa
EMS
170 def update(self):
171 """Mise à jour du fil RSS."""
011804bb
EMS
172 if not self.url:
173 return
db2999fa
EMS
174 feed = feedparser.parse(self.url)
175 for entry in feed.entries:
176 if Actualite.all_objects.filter(url=entry.link).count() == 0:
177 ts = entry.updated_parsed
178 date = datetime.date(ts.tm_year, ts.tm_mon, ts.tm_mday)
179 a = self.actualites.create(titre=entry.title,
180 texte=entry.summary_detail.value,
181 url=entry.link, date=date)
182
5212238e 183class ActualiteQuerySet(SEPQuerySet):
2f9c4d6c 184
5212238e 185 def filter_date(self, min=None, max=None):
230671ff 186 return self._filter_date('date', min=min, max=max)
da44ce68 187
011804bb
EMS
188 def filter_type(self, type):
189 return self.filter(source__type=type)
190
5212238e 191class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
c1b134f8 192
5212238e 193 def __init__(self, model=None):
4134daa0 194 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_actualites',
5212238e 195 weights=dict(titre=3))
c1b134f8 196
7fb6bd61 197 def filter_date(self, min=None, max=None):
230671ff
EMS
198 return self._filter_date('date', min=min, max=max)
199
011804bb
EMS
200 TYPE_CODES = {'actu': 1, 'appels': 2}
201 def filter_type(self, type):
202 return self.filter(type=self.TYPE_CODES[type])
203
5212238e
EMS
204class ActualiteManager(SEPManager):
205
206 def get_query_set(self):
207 return ActualiteQuerySet(self.model).filter(visible=True)
2f9c4d6c 208
5212238e
EMS
209 def get_sphinx_query_set(self):
210 return ActualiteSphinxQuerySet(self.model).order_by('-date')
2f9c4d6c 211
5212238e
EMS
212 def filter_date(self, min=None, max=None):
213 return self.get_query_set().filter_date(min=min, max=max)
bae03b7b 214
011804bb
EMS
215 def filter_type(self, type):
216 return self.get_query_set().filter_type(type)
217
d15017b2 218class Actualite(models.Model):
4f262f90 219 id = models.AutoField(primary_key=True, db_column='id_actualite')
d15017b2
CR
220 titre = models.CharField(max_length=765, db_column='titre_actualite')
221 texte = models.TextField(db_column='texte_actualite')
222 url = models.CharField(max_length=765, db_column='url_actualite')
d15017b2 223 date = models.DateField(db_column='date_actualite')
db2999fa
EMS
224 visible = models.BooleanField(db_column='visible_actualite', default=False)
225 ancienid = models.IntegerField(db_column='ancienId_actualite', blank=True, null=True)
011804bb 226 source = models.ForeignKey(SourceActualite, related_name='actualites')
3a45eb64 227 disciplines = models.ManyToManyField(Discipline, blank=True, related_name="actualites")
a5f76eb4 228 regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
6ef8ead4 229
2f9c4d6c 230 objects = ActualiteManager()
c59dba82 231 all_objects = models.Manager()
2f9c4d6c 232
d15017b2
CR
233 class Meta:
234 db_table = u'actualite'
7020ea3d 235 ordering = ["-date"]
92c7413b 236
264a3210
EMS
237 def __unicode__ (self):
238 return "%s" % (self.titre)
239
230671ff
EMS
240 def get_absolute_url(self):
241 return reverse('actualite', kwargs={'id': self.id})
242
264a3210
EMS
243 def assigner_disciplines(self, disciplines):
244 self.disciplines.add(*disciplines)
245
246 def assigner_regions(self, regions):
247 self.regions.add(*regions)
248
5212238e 249# Agenda
4101cfc0 250
5212238e 251class EvenementQuerySet(SEPQuerySet):
4101cfc0 252
5212238e
EMS
253 def filter_type(self, type):
254 return self.filter(type=type)
c1b134f8 255
5212238e
EMS
256 def filter_debut(self, min=None, max=None):
257 qs = self
258 if min:
259 qs = qs.filter(debut__gte=min)
260 if max:
261 qs = qs.filter(debut__lt=max+datetime.timedelta(days=1))
262 return qs
c1b134f8 263
5212238e 264class EvenementSphinxQuerySet(SEPSphinxQuerySet):
4101cfc0 265
5212238e 266 def __init__(self, model=None):
4134daa0 267 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_evenements',
5212238e 268 weights=dict(titre=3))
116db1fd 269
5212238e
EMS
270 def filter_type(self, type):
271 return self.add_to_query('@type "%s"' % type)
272
273 def filter_debut(self, min=None, max=None):
230671ff 274 return self._filter_date('debut', min=min, max=max)
7bbf600c 275
5212238e 276class EvenementManager(SEPManager):
5212238e 277
5212238e
EMS
278 def get_query_set(self):
279 return EvenementQuerySet(self.model).filter(approuve=True)
280
281 def get_sphinx_query_set(self):
282 return EvenementSphinxQuerySet(self.model).order_by('-debut')
283
284 def filter_type(self, type):
285 return self.get_query_set().filter_type(type)
286
287 def filter_debut(self, min=None, max=None):
288 return self.get_query_set().filter_debut(min=min, max=max)
bae03b7b 289
1719bf4e 290def build_time_zone_choices(pays=None):
1719bf4e
EMS
291 timezones = pytz.country_timezones[pays] if pays else pytz.common_timezones
292 result = []
86983865 293 now = datetime.datetime.now()
1719bf4e 294 for tzname in timezones:
86983865
EMS
295 tz = pytz.timezone(tzname)
296 fr_name = get_timezone_name(tz, locale='fr_FR')
5ffde8a4
EMS
297 try:
298 offset = tz.utcoffset(now)
299 except (AmbiguousTimeError, NonExistentTimeError):
300 # oups. On est en train de changer d'heure. Ça devrait être fini
301 # demain
302 offset = tz.utcoffset(now + datetime.timedelta(days=1))
86983865
EMS
303 seconds = offset.seconds + offset.days * 86400
304 (hours, minutes) = divmod(seconds // 60, 60)
305 offset_str = 'UTC%+d:%d' % (hours, minutes) if minutes else 'UTC%+d' % hours
1719bf4e
EMS
306 result.append((seconds, tzname, '%s - %s' % (offset_str, fr_name)))
307 result.sort()
308 return [(x[1], x[2]) for x in result]
86983865 309
92c7413b 310class Evenement(models.Model):
7bbf600c
EMS
311 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
312 (u'Conférence', u'Conférence'),
313 (u'Appel à contribution', u'Appel à contribution'),
314 (u'Journée d\'étude', u'Journée d\'étude'),
ec81ec66 315 (u'Autre', u'Autre'))
86983865
EMS
316 TIME_ZONE_CHOICES = build_time_zone_choices()
317
74b087e5 318 uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
a5f76eb4 319 approuve = models.BooleanField(default=False, verbose_name=u'approuvé')
92c7413b
CR
320 titre = models.CharField(max_length=255)
321 discipline = models.ForeignKey('Discipline', related_name = "discipline",
322 blank = True, null = True)
a5f76eb4
EMS
323 discipline_secondaire = models.ForeignKey('Discipline', related_name="discipline_secondaire",
324 verbose_name=u"discipline secondaire",
325 blank=True, null=True)
74b087e5 326 mots_cles = models.TextField('Mots-Clés', blank=True, null=True)
7bbf600c 327 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
731ef7ab
EMS
328 adresse = models.TextField()
329 ville = models.CharField(max_length=100)
330 pays = models.ForeignKey(Pays, null=True, related_name='evenements')
74b087e5
EMS
331 debut = models.DateTimeField(default=datetime.datetime.now)
332 fin = models.DateTimeField(default=datetime.datetime.now)
86983865 333 fuseau = models.CharField(max_length=100, choices=TIME_ZONE_CHOICES, verbose_name='fuseau horaire')
fe254ccc 334 description = models.TextField()
731ef7ab
EMS
335 contact = models.TextField(null=True) # champ obsolète
336 prenom = models.CharField('prénom', max_length=100)
337 nom = models.CharField(max_length=100)
338 courriel = models.EmailField()
74b087e5
EMS
339 url = models.CharField(max_length=255, blank=True, null=True)
340 piece_jointe = models.FileField(upload_to='agenda/pj', blank=True, verbose_name='pièce jointe')
fe254ccc
EMS
341 regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions additionnelles',
342 help_text="On considère d'emblée que l'évènement se déroule dans la région "
343 "dans laquelle se trouve le pays indiqué plus haut. Il est possible "
344 "de désigner ici des régions additionnelles.")
92c7413b 345
4101cfc0 346 objects = EvenementManager()
c59dba82 347 all_objects = models.Manager()
4101cfc0
EMS
348
349 class Meta:
350 ordering = ['-debut']
fe254ccc
EMS
351 verbose_name = u'évènement'
352 verbose_name_plural = u'évènements'
4101cfc0 353
230671ff 354 def __unicode__(self):
020f79a9 355 return "[%s] %s" % (self.uid, self.titre)
356
230671ff
EMS
357 def get_absolute_url(self):
358 return reverse('evenement', kwargs={'id': self.id})
359
8dfe5efa
EMS
360 def duration_display(self):
361 delta = self.fin - self.debut
362 minutes, seconds = divmod(delta.seconds, 60)
363 hours, minutes = divmod(minutes, 60)
364 days = delta.days
365 parts = []
366 if days == 1:
367 parts.append('1 jour')
368 elif days > 1:
369 parts.append('%d jours' % days)
370 if hours == 1:
371 parts.append('1 heure')
372 elif hours > 1:
373 parts.append('%d heures' % hours)
374 if minutes == 1:
375 parts.append('1 minute')
376 elif minutes > 1:
377 parts.append('%d minutes' % minutes)
378 return ' '.join(parts)
379
27fe0d70
EMS
380 def piece_jointe_display(self):
381 return self.piece_jointe and os.path.basename(self.piece_jointe.name)
382
fe254ccc
EMS
383 def courriel_display(self):
384 return self.courriel.replace(u'@', u' (à) ')
385
73309469 386 def clean(self):
387 from django.core.exceptions import ValidationError
388 if self.debut > self.fin:
389 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
390
b7a741ad 391 def save(self, *args, **kwargs):
392 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
393 approuvé"""
731ef7ab 394 self.contact = '' # Vider ce champ obsolète à la première occasion...
73309469 395 self.clean()
b7a741ad 396 super(Evenement, self).save(*args, **kwargs)
acd5cd8f 397 self.update_vevent()
b7a741ad 398
399 # methodes de commnunications avec CALDAV
400 def as_ical(self,):
401 """Retourne l'evenement django sous forme d'objet icalendar"""
402 cal = vobject.iCalendar()
403 cal.add('vevent')
404
405 # fournit son propre uid
7f56d0d4 406 if self.uid in [None, ""]:
b7a741ad 407 self.uid = str(uuid.uuid1())
408
409 cal.vevent.add('uid').value = self.uid
410
411 cal.vevent.add('summary').value = self.titre
412
413 if self.mots_cles is None:
414 kw = []
415 else:
416 kw = self.mots_cles.split(",")
417
418 try:
419 kw.append(self.discipline.nom)
420 kw.append(self.discipline_secondaire.nom)
421 kw.append(self.type)
422 except: pass
423
79b400f0 424 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
b7a741ad 425 for k in kw:
426 cal.vevent.add('x-auf-keywords').value = k
427
428 description = self.description
429 if len(kw) > 0:
430 if len(self.description) > 0:
431 description += "\n"
028f548f 432 description += u"Mots-clés: " + ", ".join(kw)
b7a741ad 433
7f214e0f
EMS
434 cal.vevent.add('dtstart').value = combine(self.debut, pytz.timezone(self.fuseau))
435 cal.vevent.add('dtend').value = combine(self.fin, pytz.timezone(self.fuseau))
b7a741ad 436 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
437 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
79b400f0 438 if len(description) > 0:
b7a741ad 439 cal.vevent.add('description').value = description
440 if len(self.contact) > 0:
441 cal.vevent.add('contact').value = self.contact
442 if len(self.url) > 0:
443 cal.vevent.add('url').value = self.url
fe254ccc 444 cal.vevent.add('location').value = ', '.join([x for x in [self.adresse, self.ville, self.pays.nom] if x])
74b087e5
EMS
445 if self.piece_jointe:
446 url = self.piece_jointe.url
447 if not url.startswith('http://'):
448 url = SITE_ROOT_URL + url
449 cal.vevent.add('attach').value = url
b7a741ad 450 return cal
451
452 def update_vevent(self,):
453 """Essaie de créer l'évènement sur le serveur ical.
454 En cas de succès, l'évènement local devient donc inactif et approuvé"""
455 try:
456 if self.approuve:
457 event = self.as_ical()
458 client = caldav.DAVClient(CALENDRIER_URL)
459 cal = caldav.Calendar(client, url = CALENDRIER_URL)
460 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
461 e.save()
462 except:
463 self.approuve = False
464
465 def delete_vevent(self,):
466 """Supprime l'evenement sur le serveur caldav"""
467 try:
468 if self.approuve:
469 event = self.as_ical()
470 client = caldav.DAVClient(CALENDRIER_URL)
471 cal = caldav.Calendar(client, url = CALENDRIER_URL)
472 e = cal.event(self.uid)
473 e.delete()
474 except error.NotFoundError:
475 pass
476
264a3210
EMS
477 def assigner_regions(self, regions):
478 self.regions.add(*regions)
479
480 def assigner_disciplines(self, disciplines):
481 if len(disciplines) == 1:
482 if self.discipline:
483 self.discipline_secondaire = disciplines[0]
484 else:
485 self.discipline = disciplines[0]
486 elif len(disciplines) >= 2:
487 self.discipline = disciplines[0]
488 self.discipline_secondaire = disciplines[1]
489
b7a741ad 490def delete_vevent(sender, instance, *args, **kwargs):
5212238e
EMS
491 # Surcharge du comportement de suppression
492 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
493 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
b7a741ad 494 instance.delete_vevent()
5212238e 495pre_delete.connect(delete_vevent, sender=Evenement)
b7a741ad 496
5212238e 497# Ressources
b7a741ad 498
d972b61d 499class ListSet(models.Model):
500 spec = models.CharField(primary_key = True, max_length = 255)
501 name = models.CharField(max_length = 255)
502 server = models.CharField(max_length = 255)
9eda5d6c 503 validated = models.BooleanField(default = True)
d972b61d 504
10d37e44 505 def __unicode__(self,):
506 return self.name
507
230671ff
EMS
508class RecordQuerySet(SEPQuerySet):
509
510 def filter_modified(self, min=None, max=None):
7ed3ee8f 511 return self._filter_date('modified', min=min, max=max)
230671ff 512
5212238e 513class RecordSphinxQuerySet(SEPSphinxQuerySet):
f153be1b 514
5212238e 515 def __init__(self, model=None):
4134daa0 516 SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_ressources',
5212238e 517 weights=dict(title=3))
c1b134f8 518
230671ff 519 def filter_modified(self, min=None, max=None):
7ed3ee8f 520 return self._filter_date('modified', min=min, max=max)
230671ff 521
5212238e 522class RecordManager(SEPManager):
f12cc7fb 523
5212238e 524 def get_query_set(self):
f153be1b
EMS
525 """Ne garder que les ressources validées et qui sont soit dans aucun
526 listset ou au moins dans un listset validé."""
230671ff 527 qs = RecordQuerySet(self.model)
5212238e 528 qs = qs.filter(validated=True)
82f25472
EMS
529 qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
530 return qs.distinct()
f153be1b 531
5212238e
EMS
532 def get_sphinx_query_set(self):
533 return RecordSphinxQuerySet(self.model)
77b0fac0 534
230671ff
EMS
535 def filter_modified(self, min=None, max=None):
536 return self.get_query_set().filter_modified(min=min, max=max)
537
0cc5f772 538class Record(models.Model):
23b5b3d5 539
540 #fonctionnement interne
0cc5f772 541 id = models.AutoField(primary_key = True)
a5f76eb4 542 server = models.CharField(max_length = 255, verbose_name=u'serveur')
23b5b3d5 543 last_update = models.CharField(max_length = 255)
544 last_checksum = models.CharField(max_length = 255)
a5f76eb4 545 validated = models.BooleanField(default=True, verbose_name=u'validé')
23b5b3d5 546
547 #OAI
18dbd2cf
EMS
548 title = models.TextField(null=True, blank=True, verbose_name=u'titre')
549 creator = models.TextField(null=True, blank=True, verbose_name=u'auteur')
550 description = models.TextField(null=True, blank=True)
551 modified = models.CharField(max_length=255, null=True, blank=True)
23b5b3d5 552 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
553 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
554 source = models.TextField(null = True, blank = True)
555 contributor = models.TextField(null = True, blank = True)
18dbd2cf 556 subject = models.TextField(null=True, blank=True, verbose_name='sujet')
23b5b3d5 557 publisher = models.TextField(null = True, blank = True)
558 type = models.TextField(null = True, blank = True)
559 format = models.TextField(null = True, blank = True)
560 language = models.TextField(null = True, blank = True)
da9020f3 561
c88d78dc 562 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
d972b61d 563
da9020f3 564 #SEP 2 (aucune données récoltées)
23b5b3d5 565 alt_title = models.TextField(null = True, blank = True)
566 abstract = models.TextField(null = True, blank = True)
567 creation = models.CharField(max_length = 255, null = True, blank = True)
568 issued = models.CharField(max_length = 255, null = True, blank = True)
569 isbn = models.TextField(null = True, blank = True)
570 orig_lang = models.TextField(null = True, blank = True)
da9020f3 571
572 # Metadata AUF multivaluées
a342f93a
EMS
573 disciplines = models.ManyToManyField(Discipline, blank=True)
574 thematiques = models.ManyToManyField(Thematique, blank=True, verbose_name='thématiques')
575 pays = models.ManyToManyField(Pays, blank=True)
576 regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
0cc5f772 577
5212238e 578 # Managers
da44ce68 579 objects = RecordManager()
c59dba82 580 all_objects = models.Manager()
da44ce68 581
18dbd2cf
EMS
582 class Meta:
583 verbose_name = 'ressource'
584
264a3210
EMS
585 def __unicode__(self):
586 return "[%s] %s" % (self.server, self.title)
587
230671ff
EMS
588 def get_absolute_url(self):
589 return reverse('ressource', kwargs={'id': self.id})
590
264a3210 591 def getServeurURL(self):
f98ad449 592 """Retourne l'URL du serveur de provenance"""
593 return RESOURCES[self.server]['url']
594
264a3210 595 def est_complet(self):
6d885e0c 596 """teste si le record à toutes les données obligatoires"""
597 return self.disciplines.count() > 0 and \
598 self.thematiques.count() > 0 and \
599 self.pays.count() > 0 and \
600 self.regions.count() > 0
601
264a3210
EMS
602 def assigner_regions(self, regions):
603 self.regions.add(*regions)
da9020f3 604
264a3210
EMS
605 def assigner_disciplines(self, disciplines):
606 self.disciplines.add(*disciplines)
264a3210 607
6d885e0c 608class Serveur(models.Model):
b7a741ad 609 """Identification d'un serveur d'ou proviennent les références"""
6d885e0c 610 nom = models.CharField(primary_key = True, max_length = 255)
611
612 def __unicode__(self,):
613 return self.nom
614
615 def conf_2_db(self,):
616 for k in RESOURCES.keys():
617 s, created = Serveur.objects.get_or_create(nom=k)
618 s.nom = k
619 s.save()
620
621class Profile(models.Model):
622 user = models.ForeignKey(User, unique=True)
623 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
0cc5f772
CR
624
625class HarvestLog(models.Model):
23b5b3d5 626 context = models.CharField(max_length = 255)
627 name = models.CharField(max_length = 255)
0cc5f772 628 date = models.DateTimeField(auto_now = True)
23b5b3d5 629 added = models.IntegerField(null = True, blank = True)
630 updated = models.IntegerField(null = True, blank = True)
a85ba76e 631 processed = models.IntegerField(null = True, blank = True)
23b5b3d5 632 record = models.ForeignKey(Record, null = True, blank = True)
633
634 @staticmethod
635 def add(message):
636 logger = HarvestLog()
637 if message.has_key('record_id'):
d566e9c1 638 message['record'] = Record.all_objects.get(id=message['record_id'])
23b5b3d5 639 del(message['record_id'])
640
641 for k,v in message.items():
642 setattr(logger, k, v)
643 logger.save()
f09bc1c6
EMS
644
645# Pages statiques
646
647class PageStatique(models.Model):
20430b8a 648 id = models.CharField(max_length=32, primary_key=True)
f09bc1c6
EMS
649 titre = models.CharField(max_length=100)
650 contenu = models.TextField()
651
652 class Meta:
653 verbose_name_plural = 'pages statiques'
fdcf5874
EMS
654
655# Recherches
656
657class GlobalSearchResults(object):
658
659 def __init__(self, actualites=None, appels=None, evenements=None,
660 ressources=None, chercheurs=None, sites=None, sites_auf=None):
661 self.actualites = actualites
662 self.appels = appels
663 self.evenements = evenements
664 self.ressources = ressources
665 self.chercheurs = chercheurs
666 self.sites = sites
667 self.sites_auf = sites_auf
668
669class Search(models.Model):
670 user = models.ForeignKey(User, editable=False)
671 content_type = models.ForeignKey(ContentType, editable=False)
672 nom = models.CharField(max_length=100, verbose_name="nom de la recherche")
673 q = models.CharField(max_length=100, blank=True, verbose_name="rechercher dans tous les champs")
674 discipline = models.ForeignKey(Discipline, blank=True, null=True)
675 region = models.ForeignKey(Region, blank=True, null=True, verbose_name='région',
676 help_text="La région est ici définie au sens, non strictement géographique, du Bureau régional de l'AUF de référence.")
677
678 def query_string(self):
679 params = dict()
680 for field in self._meta.fields:
681 if field.name in ['id', 'user', 'nom', 'search_ptr', 'content_type']:
682 continue
683 value = getattr(self, field.column)
684 if value:
685 if isinstance(value, datetime.date):
686 params[field.name] = value.strftime('%d/%m/%Y')
687 else:
688 params[field.name] = value
689 return urlencode(params)
690
691 class Meta:
692 verbose_name = 'recherche transversale'
693 verbose_name_plural = "recherches transversales"
694
695 def save(self):
696 if (not self.content_type_id):
697 self.content_type = ContentType.objects.get_for_model(self.__class__)
698 super(Search, self).save()
699
700 def as_leaf_class(self):
701 content_type = self.content_type
702 model = content_type.model_class()
703 if(model == Search):
704 return self
705 return model.objects.get(id=self.id)
706
707 def run(self):
708 from chercheurs.models import Chercheur
709 from sitotheque.models import Site
710
711 results = object()
712 actualites = Actualite.objects
713 evenements = Evenement.objects
714 ressources = Record.objects
715 chercheurs = Chercheur.objects
716 sites = Site.objects
717 if self.q:
718 actualites = actualites.search(self.q)
719 evenements = evenements.search(self.q)
720 ressources = ressources.search(self.q)
721 chercheurs = chercheurs.search(self.q)
722 sites = sites.search(self.q)
723 if self.discipline:
724 actualites = actualites.filter_discipline(self.discipline)
725 evenements = evenements.filter_discipline(self.discipline)
726 ressources = ressources.filter_discipline(self.discipline)
727 chercheurs = chercheurs.filter_discipline(self.discipline)
728 sites = sites.filter_discipline(self.discipline)
729 if self.region:
730 actualites = actualites.filter_region(self.region)
731 evenements = evenements.filter_region(self.region)
732 ressources = ressources.filter_region(self.region)
733 chercheurs = chercheurs.filter_region(self.region)
734 sites = sites.filter_region(self.region)
735 try:
736 sites_auf = google_search(0, self.q)['results']
737 except:
738 sites_auf = []
739
740 return GlobalSearchResults(
741 actualites=actualites.order_by('-date').filter_type('actu'),
742 appels=actualites.order_by('-date').filter_type('appels'),
743 evenements=evenements.order_by('-debut'),
744 ressources=ressources.order_by('-id'),
745 chercheurs=chercheurs.order_by('-date_modification'),
746 sites=sites.order_by('-date_maj'),
747 sites_auf=sites_auf
748 )
749
750 def url(self):
751 url = ''
752 if self.discipline:
753 url += '/discipline/%d' % self.discipline.id
754 if self.region:
755 url += '/region/%d' % self.region.id
756 url += '/recherche/'
757 if self.q:
758 url += '?' + urlencode({'q': self.q})
759 return url
760
da5bf7e9
EMS
761 def rss_url(self):
762 return None
763
fdcf5874
EMS
764class RessourceSearch(Search):
765 auteur = models.CharField(max_length=100, blank=True, verbose_name="auteur ou contributeur")
766 titre = models.CharField(max_length=100, blank=True)
767 sujet = models.CharField(max_length=100, blank=True)
768 publisher = models.CharField(max_length=100, blank=True, verbose_name="éditeur")
769
770 class Meta:
771 verbose_name = 'recherche de ressources'
772 verbose_name_plural = "recherches de ressources"
773
774 def run(self):
775 results = Record.objects
776 if self.q:
777 results = results.search(self.q)
778 if self.auteur:
779 results = results.add_to_query('@(creator,contributor) ' + self.auteur)
780 if self.titre:
781 results = results.add_to_query('@title ' + self.titre)
782 if self.sujet:
783 results = results.add_to_query('@subject ' + self.sujet)
784 if self.publisher:
785 results = results.add_to_query('@publisher ' + self.publisher)
786 if self.discipline:
787 results = results.filter_discipline(self.discipline)
788 if self.region:
789 results = results.filter_region(self.region)
790 if not self.q:
791 """Montrer les résultats les plus récents si on n'a pas fait
792 une recherche par mots-clés."""
230671ff 793 results = results.order_by('-modified')
fdcf5874
EMS
794 return results.all()
795
796 def url(self):
797 qs = self.query_string()
798 return reverse('ressources') + ('?' + qs if qs else '')
799
da5bf7e9
EMS
800 def rss_url(self):
801 qs = self.query_string()
802 return reverse('rss_ressources') + ('?' + qs if qs else '')
803
fdcf5874
EMS
804class ActualiteSearchBase(Search):
805 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
806 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
807
808 class Meta:
809 abstract = True
810
811 def run(self):
812 results = Actualite.objects
813 if self.q:
814 results = results.search(self.q)
815 if self.discipline:
816 results = results.filter_discipline(self.discipline)
817 if self.region:
818 results = results.filter_region(self.region)
819 if self.date_min:
820 results = results.filter_date(min=self.date_min)
821 if self.date_max:
822 results = results.filter_date(max=self.date_max)
823 return results.all()
824
825class ActualiteSearch(ActualiteSearchBase):
826
827 class Meta:
828 verbose_name = "recherche d'actualités"
829 verbose_name_plural = "recherches d'actualités"
830
831 def run(self):
832 return super(ActualiteSearch, self).run().filter_type('actu')
833
834 def url(self):
835 qs = self.query_string()
836 return reverse('actualites') + ('?' + qs if qs else '')
837
da5bf7e9
EMS
838 def rss_url(self):
839 qs = self.query_string()
840 return reverse('rss_actualites') + ('?' + qs if qs else '')
841
fdcf5874
EMS
842class AppelSearch(ActualiteSearchBase):
843
844 class Meta:
845 verbose_name = "recherche d'appels d'offres"
846 verbose_name_plural = "recherches d'appels d'offres"
847
848 def run(self):
849 return super(AppelSearch, self).run().filter_type('appel')
850
851 def url(self):
852 qs = self.query_string()
853 return reverse('appels') + ('?' + qs if qs else '')
854
da5bf7e9
EMS
855 def rss_url(self):
856 qs = self.query_string()
857 return reverse('rss_appels') + ('?' + qs if qs else '')
858
fdcf5874
EMS
859class EvenementSearch(Search):
860 titre = models.CharField(max_length=100, blank=True, verbose_name="Intitulé")
861 type = models.CharField(max_length=100, blank=True, choices=Evenement.TYPE_CHOICES)
862 date_min = models.DateField(blank=True, null=True, verbose_name="depuis le")
863 date_max = models.DateField(blank=True, null=True, verbose_name="jusqu'au")
864
865 class Meta:
866 verbose_name = "recherche d'évènements"
867 verbose_name_plural = "recherches d'évènements"
868
869 def run(self):
870 results = Evenement.objects
871 if self.q:
872 results = results.search(self.q)
873 if self.titre:
874 results = results.add_to_query('@titre ' + self.titre)
875 if self.discipline:
876 results = results.filter_discipline(self.discipline)
877 if self.region:
878 results = results.filter_region(self.region)
879 if self.type:
880 results = results.filter_type(self.type)
881 if self.date_min:
882 results = results.filter_debut(min=self.date_min)
883 if self.date_max:
884 results = results.filter_debut(max=self.date_max)
885 return results.all()
886
887 def url(self):
888 qs = self.query_string()
889 return reverse('agenda') + ('?' + qs if qs else '')
890
da5bf7e9
EMS
891 def rss_url(self):
892 qs = self.query_string()
893 return reverse('rss_agenda') + ('?' + qs if qs else '')