Commit | Line | Data |
---|---|---|
92c7413b | 1 | # -*- encoding: utf-8 -*- |
db2999fa EMS |
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 | |
5ffde8a4 | 11 | from pytz.tzinfo import AmbiguousTimeError, NonExistentTimeError |
fdcf5874 EMS |
12 | from urllib import urlencode |
13 | ||
5212238e | 14 | from backend_config import RESOURCES |
86983865 | 15 | from babel.dates import get_timezone_name |
5212238e | 16 | from caldav.lib import error |
6d885e0c | 17 | from django.contrib.auth.models import User |
fdcf5874 EMS |
18 | from django.contrib.contenttypes.models import ContentType |
19 | from django.core.urlresolvers import reverse | |
d15017b2 | 20 | from django.db import models |
15261361 | 21 | from django.db.models import Q, Max |
b7a741ad | 22 | from django.db.models.signals import pre_delete |
5212238e | 23 | from django.utils.encoding import smart_unicode |
122c4c3d | 24 | from djangosphinx.models import SphinxQuerySet, SearchError |
fdcf5874 EMS |
25 | |
26 | from datamaster_modeles.models import Region, Pays, Thematique | |
da9020f3 | 27 | from savoirs.globals import META |
74b087e5 | 28 | from settings import CALENDRIER_URL, SITE_ROOT_URL |
5212238e EMS |
29 | |
30 | # Fonctionnalités communes à tous les query sets | |
d15017b2 | 31 | |
15261361 EMS |
32 | class 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 | 45 | class 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 | |
57 | class 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 |
118 | class 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 |
140 | class 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 | 153 | class 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 | 183 | class 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 | 191 | class 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 |
204 | class 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 | 218 | class 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 | 251 | class 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 | 264 | class 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 | 276 | class 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 | 290 | def 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 | 310 | class 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 | 490 | def 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 | 495 | pre_delete.connect(delete_vevent, sender=Evenement) |
b7a741ad | 496 | |
5212238e | 497 | # Ressources |
b7a741ad | 498 | |
d972b61d | 499 | class 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 |
508 | class 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 | 513 | class 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 | 522 | class 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 | 538 | class 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 | 608 | class 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 | ||
621 | class Profile(models.Model): | |
622 | user = models.ForeignKey(User, unique=True) | |
623 | serveurs = models.ManyToManyField(Serveur, null = True, blank = True) | |
0cc5f772 CR |
624 | |
625 | class 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 | ||
647 | class 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 | ||
657 | class 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 | ||
669 | class 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 |
764 | class 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 |
804 | class 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 | ||
825 | class 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 |
842 | class 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 |
859 | class 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 '') |