Bugfix: la recherche plantait quand on ne lui donnait pas de texte à chercher.
[auf_savoirs_en_partage_django.git] / auf_savoirs_en_partage / savoirs / models.py
1 # -*- encoding: utf-8 -*-
2 import simplejson, uuid, datetime, caldav, vobject, uuid
3 from django.contrib.auth.models import User
4 from django.db import models
5 from django.db.models import Q
6 from django.db.models.signals import pre_delete
7 from timezones.fields import TimeZoneField
8 from auf_savoirs_en_partage.backend_config import RESOURCES
9 from savoirs.globals import META
10 from settings import CALENDRIER_URL
11 from datamaster_modeles.models import Thematique, Pays, Region
12 from lib.calendrier import combine
13 from caldav.lib import error
14
15 class Discipline(models.Model):
16 id = models.IntegerField(primary_key=True, db_column='id_discipline')
17 nom = models.CharField(max_length=765, db_column='nom_discipline')
18
19 def __unicode__ (self):
20 return self.nom
21
22 class Meta:
23 db_table = u'discipline'
24 ordering = ["nom",]
25
26 class SourceActualite(models.Model):
27 nom = models.CharField(max_length=255)
28 url = models.CharField(max_length=255)
29
30 def __unicode__(self,):
31 return u"%s" % self.nom
32
33 class ActualiteManager(models.Manager):
34
35 def get_query_set(self):
36 return ActualiteQuerySet(self.model)
37
38 def search(self, text):
39 return self.get_query_set().search(text)
40
41 class ActualiteQuerySet(models.query.QuerySet):
42
43 def search(self, text):
44 return self.filter(Q(titre__icontains=text) | Q(texte__icontains=text))
45
46 class Actualite(models.Model):
47 id = models.AutoField(primary_key=True, db_column='id_actualite')
48 titre = models.CharField(max_length=765, db_column='titre_actualite')
49 texte = models.TextField(db_column='texte_actualite')
50 url = models.CharField(max_length=765, db_column='url_actualite')
51 date = models.DateField(db_column='date_actualite')
52 visible = models.BooleanField(db_column='visible_actualite', default = False)
53 ancienid = models.IntegerField(db_column='ancienId_actualite', blank = True, null = True)
54 source = models.ForeignKey(SourceActualite, blank = True, null = True)
55
56 objects = ActualiteManager()
57
58 def __unicode__ (self):
59 return "%s" % (self.titre)
60
61 class Meta:
62 db_table = u'actualite'
63 ordering = ["-date",]
64
65 class EvenementManager(models.Manager):
66
67 def get_query_set(self):
68 return EvenementQuerySet(self.model)
69
70 def search(self, text):
71 return self.get_query_set().search(text)
72
73 class EvenementQuerySet(models.query.QuerySet):
74
75 def search(self, text):
76 qs = self
77 q = None
78 for word in text.split():
79 part = (Q(titre__icontains=word) |
80 Q(mots_cles__icontains=word) |
81 Q(discipline__nom__icontains=word) |
82 Q(discipline_secondaire__nom__icontains=word) |
83 Q(type__icontains=word) |
84 Q(lieu__icontains=word) |
85 Q(description__icontains=word) |
86 Q(contact__icontains=word))
87 if q is None:
88 q = part
89 else:
90 q = q & part
91 return qs.filter(q) if q is not None else qs
92
93 def search_titre(self, text):
94 qs = self
95 for word in text.split():
96 qs = qs.filter(titre__icontains=word)
97 return qs
98
99 class Evenement(models.Model):
100 TYPE_CHOICES = ((u'Colloque', u'Colloque'),
101 (u'Conférence', u'Conférence'),
102 (u'Appel à contribution', u'Appel à contribution'),
103 (u'Journée d\'étude', u'Journée d\'étude'),
104 (None, u'Autre'))
105
106 uid = models.CharField(max_length = 255, default = str(uuid.uuid1()))
107 approuve = models.BooleanField(default = False)
108 titre = models.CharField(max_length=255)
109 discipline = models.ForeignKey('Discipline', related_name = "discipline",
110 blank = True, null = True)
111 discipline_secondaire = models.ForeignKey('Discipline', related_name = \
112 "discipline_secondaire",
113 verbose_name = \
114 "Discipline secondaire",
115 blank = True, null = True)
116 mots_cles = models.TextField('Mots-Clés', blank = True, null = True)
117 type = models.CharField(max_length=255, choices=TYPE_CHOICES)
118 fuseau = TimeZoneField(verbose_name = 'Fuseau horaire')
119 debut = models.DateTimeField(default = datetime.datetime.now)
120 fin = models.DateTimeField(default = datetime.datetime.now)
121 lieu = models.TextField()
122 description = models.TextField(blank = True, null = True)
123 #fichiers = TODO?
124 contact = models.TextField(blank = True, null = True)
125 url = models.CharField(max_length=255, blank = True, null = True)
126
127 objects = EvenementManager()
128
129 class Meta:
130 ordering = ['-debut']
131
132 def __unicode__(self,):
133 return "[%s] %s" % (self.uid, self.titre)
134
135 def clean(self):
136 from django.core.exceptions import ValidationError
137 if self.debut > self.fin:
138 raise ValidationError('La date de fin ne doit pas être antérieure à la date de début')
139
140 def save(self, *args, **kwargs):
141 """Sauvegarde l'objet dans django et le synchronise avec caldav s'il a été
142 approuvé"""
143 self.clean()
144 self.update_vevent()
145 super(Evenement, self).save(*args, **kwargs)
146
147 # methodes de commnunications avec CALDAV
148 def as_ical(self,):
149 """Retourne l'evenement django sous forme d'objet icalendar"""
150 cal = vobject.iCalendar()
151 cal.add('vevent')
152
153 # fournit son propre uid
154 if self.uid in [None, ""]:
155 self.uid = str(uuid.uuid1())
156
157 cal.vevent.add('uid').value = self.uid
158
159 cal.vevent.add('summary').value = self.titre
160
161 if self.mots_cles is None:
162 kw = []
163 else:
164 kw = self.mots_cles.split(",")
165
166 try:
167 kw.append(self.discipline.nom)
168 kw.append(self.discipline_secondaire.nom)
169 kw.append(self.type)
170 except: pass
171
172 kw = [x.strip() for x in kw if len(x.strip()) > 0 and x is not None]
173 for k in kw:
174 cal.vevent.add('x-auf-keywords').value = k
175
176 description = self.description
177 if len(kw) > 0:
178 if len(self.description) > 0:
179 description += "\n"
180 description += u"Mots-clés: " + ", ".join(kw)
181
182 cal.vevent.add('dtstart').value = combine(self.debut, self.fuseau)
183 cal.vevent.add('dtend').value = combine(self.fin, self.fuseau)
184 cal.vevent.add('created').value = combine(datetime.datetime.now(), "UTC")
185 cal.vevent.add('dtstamp').value = combine(datetime.datetime.now(), "UTC")
186 if len(description) > 0:
187 cal.vevent.add('description').value = description
188 if len(self.contact) > 0:
189 cal.vevent.add('contact').value = self.contact
190 if len(self.url) > 0:
191 cal.vevent.add('url').value = self.url
192 if len(self.lieu) > 0:
193 cal.vevent.add('location').value = self.lieu
194 return cal
195
196 def update_vevent(self,):
197 """Essaie de créer l'évènement sur le serveur ical.
198 En cas de succès, l'évènement local devient donc inactif et approuvé"""
199 try:
200 if self.approuve:
201 event = self.as_ical()
202 client = caldav.DAVClient(CALENDRIER_URL)
203 cal = caldav.Calendar(client, url = CALENDRIER_URL)
204 e = caldav.Event(client, parent = cal, data = event.serialize(), id=self.uid)
205 e.save()
206 except:
207 self.approuve = False
208
209 def delete_vevent(self,):
210 """Supprime l'evenement sur le serveur caldav"""
211 try:
212 if self.approuve:
213 event = self.as_ical()
214 client = caldav.DAVClient(CALENDRIER_URL)
215 cal = caldav.Calendar(client, url = CALENDRIER_URL)
216 e = cal.event(self.uid)
217 e.delete()
218 except error.NotFoundError:
219 pass
220
221
222 # Surcharge du comportement de suppression
223 # La méthode de connexion par signals est préférable à surcharger la méthode delete()
224 # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
225 def delete_vevent(sender, instance, *args, **kwargs):
226 instance.delete_vevent()
227
228 pre_delete.connect(delete_vevent, sender = Evenement)
229
230
231 class ListSet(models.Model):
232 spec = models.CharField(primary_key = True, max_length = 255)
233 name = models.CharField(max_length = 255)
234 server = models.CharField(max_length = 255)
235 validated = models.BooleanField(default = True)
236
237 def __unicode__(self,):
238 return self.name
239
240 class RecordManager(models.Manager):
241
242 def get_query_set(self):
243 return RecordQuerySet(self.model)
244
245 def search(self, text):
246 return self.get_query_set().search(text)
247
248 def validated(self):
249 return self.get_query_set().validated()
250
251 class RecordQuerySet(models.query.QuerySet):
252
253 def search(self, text):
254 qs = self
255 words = text.split()
256
257 # Ne garder que les ressources qui contiennent tous les mots
258 # demandés.
259 q = None
260 for word in words:
261 part = (Q(title__icontains=word) | Q(description__icontains=word) |
262 Q(creator__icontains=word) | Q(contributor__icontains=word) |
263 Q(subject__icontains=word) | Q(disciplines__nom__icontains=word) |
264 Q(regions__nom__icontains=word) | Q(pays__nom__icontains=word) |
265 Q(pays__region__nom__icontains=word))
266 if q is None:
267 q = part
268 else:
269 q = q & part
270 if q is not None:
271 qs = qs.filter(q).distinct()
272
273 # On donne un point pour chaque mot présent dans le titre.
274 if words:
275 score_expr = ' + '.join(['(title LIKE %s)'] * len(words))
276 score_params = ['%' + word + '%' for word in words]
277 qs = qs.extra(
278 select={'score': score_expr},
279 select_params=score_params
280 ).order_by('-score')
281
282 return qs
283
284 def search_auteur(self, text):
285 qs = self
286 for word in text.split():
287 qs = qs.filter(Q(creator__icontains=word) | Q(contributor__icontains=word))
288 return qs
289
290 def search_sujet(self, text):
291 qs = self
292 for word in text.split():
293 qs = qs.filter(subject__icontains=word)
294 return qs
295
296 def search_titre(self, text):
297 qs = self
298 for word in text.split():
299 qs = qs.filter(title__icontains=word)
300 return qs
301
302 def validated(self):
303 """Ne garder que les ressources validées et qui sont soit dans aucun
304 listset ou au moins dans un listset validé."""
305 qs = self.filter(validated=True)
306 qs = qs.extra(where=['''((savoirs_record.id NOT IN (SELECT record_id FROM savoirs_record_listsets)) OR
307 ((SELECT MAX(l.validated) FROM savoirs_listset l
308 INNER JOIN savoirs_record_listsets rl ON rl.listset_id = l.spec
309 WHERE rl.record_id = savoirs_record.id) = TRUE))'''])
310 return qs
311
312 class Record(models.Model):
313
314 #fonctionnement interne
315 id = models.AutoField(primary_key = True)
316 server = models.CharField(max_length = 255)
317 last_update = models.CharField(max_length = 255)
318 last_checksum = models.CharField(max_length = 255)
319 validated = models.BooleanField(default = True)
320
321 #OAI
322 title = models.TextField(null = True, blank = True)
323 creator = models.TextField(null = True, blank = True)
324 description = models.TextField(null = True, blank = True)
325 modified = models.CharField(max_length = 255, null = True, blank = True)
326 identifier = models.CharField(max_length = 255, null = True, blank = True, unique = True)
327 uri = models.CharField(max_length = 255, null = True, blank = True, unique = True)
328 source = models.TextField(null = True, blank = True)
329 contributor = models.TextField(null = True, blank = True)
330 subject = models.TextField(null = True, blank = True)
331 publisher = models.TextField(null = True, blank = True)
332 type = models.TextField(null = True, blank = True)
333 format = models.TextField(null = True, blank = True)
334 language = models.TextField(null = True, blank = True)
335
336 listsets = models.ManyToManyField(ListSet, null = True, blank = True)
337
338 #SEP 2 (aucune données récoltées)
339 alt_title = models.TextField(null = True, blank = True)
340 abstract = models.TextField(null = True, blank = True)
341 creation = models.CharField(max_length = 255, null = True, blank = True)
342 issued = models.CharField(max_length = 255, null = True, blank = True)
343 isbn = models.TextField(null = True, blank = True)
344 orig_lang = models.TextField(null = True, blank = True)
345
346 # Metadata AUF multivaluées
347 disciplines = models.ManyToManyField(Discipline)
348 thematiques = models.ManyToManyField(Thematique)
349 pays = models.ManyToManyField(Pays)
350 regions = models.ManyToManyField(Region)
351
352 # Manager
353 objects = RecordManager()
354
355 def getServeurURL(self,):
356 """Retourne l'URL du serveur de provenance"""
357 return RESOURCES[self.server]['url']
358
359 def est_complet(self,):
360 """teste si le record à toutes les données obligatoires"""
361 return self.disciplines.count() > 0 and \
362 self.thematiques.count() > 0 and \
363 self.pays.count() > 0 and \
364 self.regions.count() > 0
365
366 def __unicode__(self):
367 return "[%s] %s" % (self.server, self.title)
368
369 class Serveur(models.Model):
370 """Identification d'un serveur d'ou proviennent les références"""
371 nom = models.CharField(primary_key = True, max_length = 255)
372
373 def __unicode__(self,):
374 return self.nom
375
376 def conf_2_db(self,):
377 for k in RESOURCES.keys():
378 s, created = Serveur.objects.get_or_create(nom=k)
379 s.nom = k
380 s.save()
381
382 class Profile(models.Model):
383 user = models.ForeignKey(User, unique=True)
384 serveurs = models.ManyToManyField(Serveur, null = True, blank = True)
385
386 class HarvestLog(models.Model):
387 context = models.CharField(max_length = 255)
388 name = models.CharField(max_length = 255)
389 date = models.DateTimeField(auto_now = True)
390 added = models.IntegerField(null = True, blank = True)
391 updated = models.IntegerField(null = True, blank = True)
392 processed = models.IntegerField(null = True, blank = True)
393 record = models.ForeignKey(Record, null = True, blank = True)
394
395 @staticmethod
396 def add(message):
397 logger = HarvestLog()
398 if message.has_key('record_id'):
399 message['record'] = Record.objects.get(id=message['record_id'])
400 del(message['record_id'])
401
402 for k,v in message.items():
403 setattr(logger, k, v)
404 logger.save()