Test coverage report: calendarium.models

# calendarium.models: 163 total statements, 85.5% covered

Generated: Thu 2013-05-02 12:21 SGT

Source file: /Users/martin/Repos/django-calendarium/calendarium/models.py

Stats: 118 executed, 20 missed, 25 excluded, 273 ignored

1. `"""`
2. `Models for the ``calendarium`` app.`
3. ``
4. `The code of these models is highly influenced by or taken from the models of`
5. `django-schedule:`
6. ``
7. `https://github.com/thauber/django-schedule/tree/master/schedule/models`
8. ``
9. `"""`
10. `import json`
11. `from dateutil import rrule`
12. ``
13. `from django.contrib.contenttypes import generic`
14. `from django.contrib.contenttypes.models import ContentType`
15. `from django.core.urlresolvers import reverse`
16. `from django.core.validators import RegexValidator`
17. `from django.db import models`
18. `from django.utils.timezone import timedelta`
19. `from django.utils.translation import ugettext_lazy as _`
20. ``
21. `from calendarium.constants import FREQUENCY_CHOICES, OCCURRENCE_DECISIONS`
22. `from calendarium.utils import OccurrenceReplacer`
23. `from calendarium.widgets import ColorPickerWidget`
24. `from south.modelsinspector import add_introspection_rules`
25. ``
26. ``
27. `class ColorField(models.CharField):`
28. `"""Custom color field to display a color picker."""`
29. `def __init__(self, *args, **kwargs):`
30. `kwargs['max_length'] = 6`
31. `super(ColorField, self).__init__(*args, **kwargs)`
32. `self.validators.append(RegexValidator(`
33. `regex='^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',`
34. `message='Only RGB color model inputs allowed, like 00000',`
35. `code='nomatch'))`
36. ``
37. `def formfield(self, **kwargs):`
38. `kwargs['widget'] = ColorPickerWidget`
39. `return super(ColorField, self).formfield(**kwargs)`
40. ``
41. ``
42. `add_introspection_rules([], ["^calendarium\.models\.ColorField"])`
43. ``
44. ``
45. `class EventModelManager(models.Manager):`
46. `"""Custom manager for the ``Event`` model class."""`
47. `def get_occurrences(self, start, end, category=None):`
48. `"""Returns a list of events and occurrences for the given period."""`
49. `# we always want the time of start and end to be at 00:00`
50. `start = start.replace(minute=0, hour=0)`
51. `end = end.replace(minute=0, hour=0)`
52. `# if we recieve the date of one day as start and end, we need to set`
53. `# end one day forward`
54. `if start == end:`
55. `end = start + timedelta(days=1)`
56. ``
57. `# retrieving relevant events`
58. `# TODO currently for events with a rule, I can't properly find out when`
59. `# the last occurrence of the event ends, or find a way to filter that,`
60. `# so I'm still fetching **all** events before this period, that have a`
61. `# end_recurring_period.`
62. `# For events without a rule, I fetch only the relevant ones.`
63. `qs = self.get_query_set()`
64. `if category:`
65. `relevant_events = qs.filter(start__lt=end, category=category)`
66. `else:`
67. `relevant_events = qs.filter(start__lt=end)`
68. `# get all occurrences for those events that don't already have a`
69. `# persistent match and that lie in this period.`
70. `all_occurrences = []`
71. `for event in relevant_events:`
72. `all_occurrences.extend(event.get_occurrences(start, end))`
73. ``
74. `# sort and return`
75. `return sorted(all_occurrences, key=lambda x: x.start)`
76. ``
77. ``
78. `class EventModelMixin(models.Model):`
79. `"""`
80. `Abstract base class to prevent code duplication.`
81. `:start: The start date of the event.`
82. `:end: The end date of the event.`
83. `:creation_date: When this event was created.`
84. `:description: The description of the event.`
85. ``
86. `"""`
87. `start = models.DateTimeField(`
88. `verbose_name=_('Start date'),`
89. `)`
90. ``
91. `end = models.DateTimeField(`
92. `verbose_name=_('End date'),`
93. `)`
94. ``
95. `creation_date = models.DateTimeField(`
96. `verbose_name=_('Creation date'),`
97. `auto_now_add=True,`
98. `)`
99. ``
100. `description = models.TextField(`
101. `max_length=2048,`
102. `verbose_name=_('Description'),`
103. `blank=True,`
104. `)`
105. ``
106. `def __unicode__(self):`
107. `return self.title`
108. ``
109. `class Meta:`
110. `abstract = True`
111. ``
112. ``
113. `class Event(EventModelMixin):`
114. `"""`
115. `Hold the information about an event in the calendar.`
116. ``
117. `:created_by: FK to the ``User``, who created this event.`
118. `:category: FK to the ``EventCategory`` this event belongs to.`
119. `:rule: FK to the definition of the recurrence of an event.`
120. `:end_recurring_period: The possible end of the recurring definition.`
121. `:title: The title of the event.`
122. ``
123. `"""`
124. ``
125. `created_by = models.ForeignKey(`
126. `'auth.User',`
127. `verbose_name=_('Created by'),`
128. `related_name='events',`
129. `blank=True, null=True,`
130. `)`
131. ``
132. `category = models.ForeignKey(`
133. `'EventCategory',`
134. `verbose_name=_('Category'),`
135. `related_name='events',`
136. `)`
137. ``
138. `rule = models.ForeignKey(`
139. `'Rule',`
140. `verbose_name=_('Rule'),`
141. `blank=True, null=True,`
142. `)`
143. ``
144. `end_recurring_period = models.DateTimeField(`
145. `verbose_name=_('End of recurring'),`
146. `blank=True, null=True,`
147. `)`
148. ``
149. `title = models.CharField(`
150. `max_length=256,`
151. `verbose_name=_('Title'),`
152. `)`
153. ``
154. `objects = EventModelManager()`
155. ``
156. `def get_absolute_url(self):`
157. `return reverse('calendar_event_detail', kwargs={'pk': self.pk})`
158. ``
159. `def _create_occurrence(self, occ_start, occ_end=None):`
160. `"""Creates an Occurrence instance."""`
161. `# if the length is not altered, it is okay to only pass occ_start`
162. `if not occ_end:`
163. `occ_end = occ_start + (self.end - self.start)`
164. `return Occurrence(`
165. `event=self, start=occ_start, end=occ_end,`
166. `# TODO not sure why original start and end also are occ_start/_end`
167. `original_start=occ_start, original_end=occ_end,`
168. `title=self.title, description=self.description,`
169. `creation_date=self.creation_date, created_by=self.created_by)`
170. ``
171. `def _get_date_gen(self, rr, start, end):`
172. `"""Returns a generator to create the start dates for occurrences."""`
173. `date = rr.after(start)`
174. `while end and date <= end or not(end):`
175. `yield date`
176. `date = rr.after(date)`
177. ``
178. `def _get_occurrence_gen(self, start, end):`
179. `"""Computes all occurrences for this event from start to end."""`
180. `# get length of the event`
181. `length = self.end - self.start`
182. ``
183. `if self.rule:`
184. `# if the end of the recurring period is before the end arg passed`
185. `# the end of the recurring period should be the new end`
186. `if self.end_recurring_period and end and (`
187. `self.end_recurring_period < end):`
188. `end = self.end_recurring_period`
189. `# making start date generator`
190. `occ_start_gen = self._get_date_gen(`
191. `self.get_rrule_object(),`
192. `start - length, end)`
193. ``
194. `# chosing the first item from the generator to initiate`
195. `occ_start = occ_start_gen.next()`
196. `while not end or (end and occ_start <= end):`
197. `occ_end = occ_start + length`
198. `yield self._create_occurrence(occ_start, occ_end)`
199. `occ_start = occ_start_gen.next()`
200. `else:`
201. `# check if event is in the period`
202. `if (not end or self.start < end) and self.end >= start:`
203. `yield self._create_occurrence(self.start, self.end)`
204. ``
205. `def get_occurrences(self, start, end=None):`
206. `"""Returns all occurrences from start to end."""`
207. `# get persistent occurrences`
208. `persistent_occurrences = self.occurrences.all()`
209. ``
210. `# setup occ_replacer with p_occs`
211. `occ_replacer = OccurrenceReplacer(persistent_occurrences)`
212. ``
213. `# compute own occurrences according to rule that overlap with the`
214. `# period`
215. `occurrence_gen = self._get_occurrence_gen(start, end)`
216. `# get additional occs, that we need to take into concern`
217. `additional_occs = occ_replacer.get_additional_occurrences(`
218. `start, end)`
219. `occ = occurrence_gen.next()`
220. `while not end or (occ.start < end or any(additional_occs)):`
221. `if occ_replacer.has_occurrence(occ):`
222. `p_occ = occ_replacer.get_occurrence(occ)`
223. ``
224. `# if the persistent occ falls into the period, replace it`
225. `if (end and p_occ.start < end) and p_occ.end >= start:`
226. `estimated_occ = p_occ`
227. `else:`
228. `occ = occurrence_gen.next()`
229. `continue`
230. `else:`
231. `# if there is no persistent match, use the original occ`
232. `estimated_occ = occ`
233. ``
234. `if any(additional_occs) and (`
235. `estimated_occ.start == additional_occs[0].start):`
236. `final_occ = additional_occs.pop(0)`
237. `else:`
238. `final_occ = estimated_occ`
239. `if not final_occ.cancelled:`
240. `yield final_occ`
241. `occ = occurrence_gen.next()`
242. ``
243. `def get_rrule_object(self):`
244. `"""Returns the rrule object for this ``Event``."""`
245. `if self.rule:`
246. `params = self.rule.get_params()`
247. `frequency = 'rrule.{0}'.format(self.rule.frequency)`
248. `return rrule.rrule(eval(frequency), dtstart=self.start, **params)`
249. ``
250. ``
251. `class EventCategory(models.Model):`
252. `"""The category of an event."""`
253. `name = models.CharField(`
254. `max_length=256,`
255. `verbose_name=_('Name'),`
256. `)`
257. ``
258. `color = ColorField(`
259. `verbose_name=_('Color'),`
260. `)`
261. ``
262. `def __unicode__(self):`
263. `return self.name`
264. ``
265. ``
266. `class EventRelation(models.Model):`
267. `"""`
268. `This class allows to relate additional or external data to an event.`
269. ``
270. `:event: A FK to the ``Event`` this additional data is related to.`
271. `:content_type: A FK to ContentType of the generic object.`
272. `:object_id: The id of the generic object.`
273. `:content_object: The generic foreign key to the generic object.`
274. `:relation_type: A string representing the type of the relation. This allows`
275. `to relate to the same content_type several times but mean different`
276. `things, such as (normal_guests, speakers, keynote_speakers, all being`
277. `Guest instances)`
278. ``
279. `"""`
280. ``
281. `event = models.ForeignKey(`
282. `'Event',`
283. `verbose_name=_("Event"),`
284. `)`
285. ``
286. `content_type = models.ForeignKey(`
287. `ContentType,`
288. `)`
289. ``
290. `object_id = models.IntegerField()`
291. ``
292. `content_object = generic.GenericForeignKey(`
293. `'content_type',`
294. `'object_id',`
295. `)`
296. ``
297. `relation_type = models.CharField(`
298. `verbose_name=_('Relation type'),`
299. `max_length=32,`
300. `blank=True, null=True,`
301. `)`
302. ``
303. `def __unicode__(self):`
304. `return 'type "{0}" for "{1}"'.format(`
305. `self.relation_type, self.event.title)`
306. ``
307. ``
308. `class Occurrence(EventModelMixin):`
309. `"""`
310. `Needed if one occurrence of an event has slightly different settings than`
311. `all other.`
312. ``
313. `:created_by: FK to the ``User``, who created this event.`
314. `:event: FK to the ``Event`` this ``Occurrence`` belongs to.`
315. `:original_start: The original start of the related ``Event``.`
316. `:original_end: The original end of the related ``Event``.`
317. `:title: The title of the event.`
318. ``
319. `"""`
320. `created_by = models.ForeignKey(`
321. `'auth.User',`
322. `verbose_name=_('Created by'),`
323. `related_name='occurrences',`
324. `blank=True, null=True,`
325. `)`
326. ``
327. `event = models.ForeignKey(`
328. `'Event',`
329. `verbose_name=_('Event'),`
330. `related_name='occurrences',`
331. `)`
332. ``
333. `original_start = models.DateTimeField(`
334. `verbose_name=_('Original start'),`
335. `)`
336. ``
337. `original_end = models.DateTimeField(`
338. `verbose_name=_('Original end'),`
339. `)`
340. ``
341. `cancelled = models.BooleanField(`
342. `verbose_name=_('Cancelled'),`
343. `)`
344. ``
345. `title = models.CharField(`
346. `max_length=256,`
347. `verbose_name=_('Title'),`
348. `blank=True,`
349. `)`
350. ``
351. `def category(self):`
352. `return self.event.category`
353. ``
354. `def delete_period(self, period):`
355. `"""Deletes a set of occurrences based on the given decision."""`
356. `# check if this is the last or only one`
357. `is_last = False`
358. `is_only = False`
359. `gen = self.event.get_occurrences(`
360. `self.start, self.event.end_recurring_period)`
361. `occs = [occ for occ in gen]`
362. `if len(occs) == 1:`
363. `is_only = True`
364. `elif len(occs) > 1 and self == occs[-1]:`
365. `is_last = True`
366. `if period == OCCURRENCE_DECISIONS['all']:`
367. `# delete all persistent occurrences along with the parent event`
368. `self.event.occurrences.all().delete()`
369. `self.event.delete()`
370. `elif period == OCCURRENCE_DECISIONS['this one']:`
371. `# check if it is the last one. If so, shorten the recurring period,`
372. `# otherwise cancel the event`
373. `if is_last:`
374. `self.event.end_recurring_period = self.start - timedelta(`
375. `days=1)`
376. `self.event.save()`
377. `elif is_only:`
378. `self.event.occurrences.all().delete()`
379. `self.event.delete()`
380. `else:`
381. `self.cancelled = True`
382. `self.save()`
383. `elif period == OCCURRENCE_DECISIONS['following']:`
384. `# just shorten the recurring period`
385. `self.event.end_recurring_period = self.start - timedelta(days=1)`
386. `self.event.occurrences.filter(start__gte=self.start).delete()`
387. `if is_only:`
388. `self.event.delete()`
389. `else:`
390. `self.event.save()`
391. ``
392. `def get_absolute_url(self):`
393. `return reverse(`
394. `'calendar_occurrence_detail', kwargs={`
395. `'pk': self.event.pk, 'year': self.start.year,`
396. `'month': self.start.month, 'day': self.start.day})`
397. ``
398. ``
399. `class Rule(models.Model):`
400. `"""`
401. `This defines the rule by which an event will recur.`
402. ``
403. `:name: Name of this rule.`
404. `:description: Description of this rule.`
405. `:frequency: A string representing the frequency of the recurrence.`
406. `:params: JSON string to hold the exact rule parameters as used by`
407. `dateutil.rrule to define the pattern of the recurrence.`
408. ``
409. `"""`
410. `name = models.CharField(`
411. `verbose_name=_("name"),`
412. `max_length=32,`
413. `)`
414. ``
415. `description = models.TextField(`
416. `_("description"),`
417. `)`
418. ``
419. `frequency = models.CharField(`
420. `verbose_name=_("frequency"),`
421. `choices=FREQUENCY_CHOICES,`
422. `max_length=10,`
423. `)`
424. ``
425. `params = models.TextField(`
426. `verbose_name=_("params"),`
427. `blank=True, null=True,`
428. `)`
429. ``
430. `def __unicode__(self):`
431. `return self.name`
432. ``
433. `def get_params(self):`
434. `if self.params:`
435. `return json.loads(self.params)`
436. `return {}`