33 | Do you want to cancel this occurrence or delete all occurrences of this event?
34 |
35 |
36 |
37 | Do you want to edit this occurrence or all occurrences?
38 |
39 |
40 |
--------------------------------------------------------------------------------
/LICENCSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008, Tony Hauber
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above
11 | copyright notice, this list of conditions and the following
12 | disclaimer in the documentation and/or other materials provided
13 | with the distribution.
14 | * Neither the name of the author nor the names of other
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/schedule/templates/schedule/event_form_base.html:
--------------------------------------------------------------------------------
1 | {% extends "schedule/base.html" %}
2 |
3 | {% block extra_head %}
4 | {{ block.super }}
5 |
6 |
36 | {% endblock %}
37 |
38 |
--------------------------------------------------------------------------------
/schedule/templates/schedule/occurrence.html:
--------------------------------------------------------------------------------
1 | {% extends "schedule/base.html" %}
2 | {% load i18n %}
3 | {% load scheduletags %}
4 |
5 | {% block body %}
6 |
{% blocktrans with event.start|date:_("DATETIME_FORMAT") as start_date %}{{ start_date }}{% endblocktrans %}
39 |
40 |
41 |
Ends
42 |
{% blocktrans with event.end|date:_("DATETIME_FORMAT") as end_date %}{{ end_date }}{% endblocktrans %}
43 |
44 |
45 |
Reoccurs
46 | {% if event.rule %}
47 |
{{ event.rule }} until {% blocktrans with event.end_recurring_period|date:_("DATETIME_FORMAT") as end_recurring_date %}{{ end_recurring_date }}{% endblocktrans %}
48 | {% else %}
49 |
{% trans "Never. This is a 'one time only' event." %}
50 | {% endif %}
51 |
52 | {% if event.description %}
53 |
Description
54 |
{{event.description}}
55 | {% endif %}
56 |
57 | {% endblock %}
58 |
--------------------------------------------------------------------------------
/schedule/conf/settings.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import ugettext, ugettext_lazy as _
2 | from django.conf import settings
3 | from django.core.exceptions import ImproperlyConfigured
4 |
5 | fdow_default = 0 # Sunday
6 |
7 | # Look for FIRST_DAY_OF_WEEK as a locale setting
8 | fdow = ugettext('FIRST_DAY_OF_WEEK')
9 | try:
10 | FIRST_DAY_OF_WEEK = int(fdow)
11 | except ValueError:
12 | # Let's try our settings
13 | fdow = getattr(settings, 'FIRST_DAY_OF_WEEK', fdow_default)
14 | FIRST_DAY_OF_WEEK = int(fdow)
15 | except ValueError:
16 | raise ImproperlyConfigured("FIRST_DAY_OF_WEEK must be an integer between 0 and 6")
17 |
18 |
19 | # whether to display cancelled occurrences
20 | # (if they are displayed then they have a css class "cancelled")
21 | # this controls behaviour of Period.classify_occurrence method
22 | SHOW_CANCELLED_OCCURRENCES = getattr(settings, 'SHOW_CANCELLED_OCCURRENCES',
23 | False)
24 |
25 | # Callable used to check if a user has edit permissions to event
26 | # (and occurrence). Used by check_edit_permission decorator
27 | # if ob==None we check permission to add occurrence
28 | CHECK_PERMISSION_FUNC = getattr(settings, 'CHECK_PERMISSION_FUNC', None)
29 | if not CHECK_PERMISSION_FUNC:
30 | def check_edit_permission(ob, user):
31 | return user.is_authenticated()
32 |
33 | CHECK_PERMISSION_FUNC = check_edit_permission
34 |
35 | # Callable used to customize the event list given for a calendar and user
36 | # (e.g. all events on that calendar, those events plus another calendar's events,
37 | # or the events filtered based on user permissions)
38 | # Imports have to be placed within the function body to avoid circular imports
39 | GET_EVENTS_FUNC = getattr(settings, 'GET_EVENTS_FUNC', None)
40 | if not GET_EVENTS_FUNC:
41 | def get_events(request, calendar):
42 | return calendar.event_set.all()
43 |
44 | GET_EVENTS_FUNC = get_events
45 |
46 | # URL to redirect to to after an occurrence is canceled
47 | OCCURRENCE_CANCEL_REDIRECT = getattr(settings, 'OCCURRENCE_CANCEL_REDIRECT', None)
48 |
--------------------------------------------------------------------------------
/schedule/templates/profiles/schedule.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load scheduletags %}
3 |
4 |
51 |
52 |
--------------------------------------------------------------------------------
/schedule/feeds/__init__.py:
--------------------------------------------------------------------------------
1 | from schedule.models import Calendar
2 | from django.contrib.syndication.feeds import FeedDoesNotExist
3 | from django.core.exceptions import ObjectDoesNotExist
4 | from django.conf import settings
5 | from schedule.feeds.atom import Feed
6 | from schedule.feeds.icalendar import ICalendarFeed
7 | from django.http import HttpResponse
8 | import datetime, itertools
9 |
10 | class UpcomingEventsFeed(Feed):
11 | feed_id = "upcoming"
12 |
13 | def feed_title(self, obj):
14 | return "Upcoming Events for %s" % obj.name
15 |
16 | def get_object(self, bits):
17 | if len(bits) != 1:
18 | raise ObjectDoesNotExist
19 | return Calendar.objects.get(pk=bits[0])
20 |
21 | def link(self, obj):
22 | if not obj:
23 | raise FeedDoesNotExist
24 | return obj.get_absolute_url()
25 |
26 | def items(self, obj):
27 | return itertools.islice(obj.occurrences_after(datetime.datetime.now()),
28 | getattr(settings, "FEED_LIST_LENGTH", 10))
29 |
30 | def item_id(self, item):
31 | return str(item.id)
32 |
33 | def item_title(self, item):
34 | return item.event.title
35 |
36 | def item_authors(self, item):
37 | if item.event.creator is None:
38 | return [{'name': ''}]
39 | return [{"name": item.event.creator.username}]
40 |
41 | def item_updated(self, item):
42 | return item.event.created_on
43 |
44 | def item_content(self, item):
45 | return "%s \n %s" % (item.event.title, item.event.description)
46 |
47 |
48 | class CalendarICalendar(ICalendarFeed):
49 | def items(self):
50 | cal_id = self.args[1]
51 | cal = Calendar.objects.get(pk=cal_id)
52 |
53 | return cal.events.all()
54 |
55 | def item_uid(self, item):
56 | return str(item.id)
57 |
58 | def item_start(self, item):
59 | return item.start
60 |
61 | def item_end(self, item):
62 | return item.end
63 |
64 | def item_summary(self, item):
65 | return item.title
66 |
67 | def item_created(self, item):
68 | return item.created_on
--------------------------------------------------------------------------------
/schedule/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 |
4 | from django.test import TestCase
5 | from django.core.urlresolvers import reverse
6 |
7 | from schedule.models import Event, Rule, Occurrence, Calendar
8 | from schedule.periods import Period, Month, Day
9 | from schedule.utils import EventListManager
10 |
11 | class TestEventListManager(TestCase):
12 | def setUp(self):
13 | weekly = Rule(frequency = "WEEKLY")
14 | weekly.save()
15 | daily = Rule(frequency = "DAILY")
16 | daily.save()
17 | cal = Calendar(name="MyCal")
18 | cal.save()
19 |
20 | self.event1 = Event(**{
21 | 'title': 'Weekly Event',
22 | 'start': datetime.datetime(2009, 4, 1, 8, 0),
23 | 'end': datetime.datetime(2009, 4, 1, 9, 0),
24 | 'end_recurring_period' : datetime.datetime(2009, 10, 5, 0, 0),
25 | 'rule': weekly,
26 | 'calendar': cal
27 | })
28 | self.event1.save()
29 | self.event2 = Event(**{
30 | 'title': 'Recent Event',
31 | 'start': datetime.datetime(2008, 1, 5, 9, 0),
32 | 'end': datetime.datetime(2008, 1, 5, 10, 0),
33 | 'end_recurring_period' : datetime.datetime(2009, 5, 5, 0, 0),
34 | 'rule': daily,
35 | 'calendar': cal
36 | })
37 | self.event2.save()
38 |
39 | def test_occurrences_after(self):
40 | eml = EventListManager([self.event1, self.event2])
41 | occurrences = eml.occurrences_after(datetime.datetime(2009,4,1,0,0))
42 | self.assertEqual(occurrences.next().event, self.event1)
43 | self.assertEqual(occurrences.next().event, self.event2)
44 | self.assertEqual(occurrences.next().event, self.event2)
45 | self.assertEqual(occurrences.next().event, self.event2)
46 | self.assertEqual(occurrences.next().event, self.event2)
47 | self.assertEqual(occurrences.next().event, self.event2)
48 | self.assertEqual(occurrences.next().event, self.event2)
49 | self.assertEqual(occurrences.next().event, self.event2)
50 | self.assertEqual(occurrences.next().event, self.event1)
51 |
--------------------------------------------------------------------------------
/docs/utils.txt:
--------------------------------------------------------------------------------
1 | =========
2 | Utilities
3 | =========
4 |
5 | There are some utility classes found in the utils module that help with certain tasks.
6 |
7 | EventListManager
8 | ----------------
9 |
10 | EventListManager objects are instantiated with a list of events. That list of events dictates the following methods
11 |
12 | ``occurrences_after(after)``
13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 |
15 | Creates a generator that produces the next occurrence inclusively after the datetime ``after``.
16 |
17 | OccurrenceReplacer
18 | ------------------
19 |
20 | If you get more into the internals of django-schedule, and decide to create your own method for producing occurrences, instead of using one of the public facing methods for this, you are going to want to replace the occurrence you produce with a persisted one, if a persisted one exists. To facilitate this in a standardized way you have the OccurrenceReplacer class.
21 |
22 | To instantiate it you give it the pool of persisted occurrences you would like to check in.
23 |
24 | >>> persisted_occurrences = my_event.occurrence_set.all()
25 | >>> occ_replacer = OccurrenceReplacer(persisted_occurrences)
26 |
27 | Now you have two convenient methods
28 |
29 | ``get_occurrence(occurrence)``
30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31 |
32 | This method returns either the passed in occurrence or the equivalent persisted occurrences from the pool of persisted occurrences this OccurrenceReplacer was instantiated with.
33 |
34 | >>> # my_generated_occurrence is an occurrence that was programatically
35 | >>> # generated from an event
36 | >>> occurrence = occ_replacer.get_occurrence(my_generated_occurrence)
37 |
38 | ``has_occurrence(occurrence)``
39 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 |
41 | This method returns a boolean. It returns True of the OccurrenceReplacer has an occurrence it would like to replace with the give occurrence, and false if it does not
42 |
43 | >>> hasattr(my_generated_occurrence, 'pk')
44 | False
45 | >>> occ_replacer.has_occurrence(my_generated_occurrence)
46 | True
47 | >>> occurrence = occ_replacer.get_occurrence(my_generated_occurrence)
48 | >>> hasattr(occurrence, 'pk')
49 | True
50 | >>> # Now with my_other_occurrence which does not have a persisted counterpart
51 | >>> hasattr(my_other_occurrence, 'pk')
52 | False
53 | >>> occ_replacer.has_occurrence(my_other_occurrence)
54 | False
55 | >>> occurrence = occ_replacer.get_occurrence(my_other_occurrence)
56 | >>> hasattr(occurrence, 'pk')
57 | False
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. Django-schedule
2 |
3 | A calendaring/scheduling application, featuring:
4 |
5 | * one-time and recurring events
6 | * calendar exceptions (occurrences changed or cancelled)
7 | * occurrences accessible through Event API and Period API
8 | * relations of events to generic objects
9 | * ready to use, nice user interface
10 | * view day, week, month, three months and year
11 | * project sample which can be launched immediately and reused in your project
12 |
13 | See "wiki page":http://wiki.github.com/bartekgorny/django-schedule/wiki for more
14 |
15 | h2. Installation
16 |
17 | Download the code; put in into your project's directory or run
python setup.py install
to install system-wide.
18 |
19 | REQUIREMENTS:
20 |
21 | * python-vobject
22 | * python-dateutil from Labix
23 |
24 | (both come with most distributions)
25 |
26 | h2. Settings.py
27 |
28 | h3. REQUIRED
29 |
30 | INSTALLED_APPS - add:
31 | 'schedule'
32 |
33 | TEMPLATE_CONTEXT_PROCESSORS - add:
34 | "django.core.context_processors.request"
35 |
36 | h4. Optional
37 |
38 | FIRST_DAY_OF_WEEK
39 |
40 | This setting determines which day of the week your calendar begins on if your locale doesn't already set it. Default is 0, which is Sunday.
41 |
42 | OCCURRENCE_CANCEL_REDIRECT
43 |
44 | This setting controls the behavior of :func:`Views.get_next_url`. If set, all calendar modifications will redirect here (unless there is a `next` set in the request.)
45 |
46 | SHOW_CANCELLED_OCCURRENCES
47 |
48 | This setting controls the behavior of :func:`Period.classify_occurence`. If True, then occurences that have been cancelled will be displayed with a css class of canceled, otherwise they won't appear at all.
49 |
50 | Defaults to False
51 |
52 | CHECK_PERMISSION_FUNC
53 |
54 | This setting controls the callable used to determine if a user has permission to edit an event or occurance. The callable must take the object and the user and return a boolean.
55 |
56 | Default:
57 |
61 |
62 | If ob is None, then the function is checking for permission to add new events
63 |
64 | GET_EVENTS_FUNC
65 |
66 | This setting controls the callable that gets all events for calendar display. The callable must take the request and the calendar and return a `QuerySet` of events. Modifying this setting allows you to pull events from multiple calendars or to filter events based on permissions
67 |
68 | Default:
69 |
This project is a scheduling app. At its core are events that constitute a start and end date implemented via two DateTimeFields together with a some meta datax such as title, description, etc.
15 |
To make it more user friendly there is the idea of EventRelation which links generic objects and an Event object. The idea is that you will keep more information about the event in these relations such as who is authorized to view them. It could be the case that User is linked to Event to represent who is authorized to view the event, and also User is linked to event to represent who is attending the event.
16 |
The EventRelation has another field called distinction. So in the previous example we could add an EventRelation which links a User to an Event with the distinction "viewer" and another EventRelation which links a User to an Event with the distinction "attendee."
')
101 | // add to document
102 | .appendTo(document.body)
103 | // hide it at first
104 | .hide();
105 |
106 | // apply bgiframe if available
107 | if ( $.fn.bgiframe )
108 | helper.parent.bgiframe();
109 |
110 | // save references to title and url elements
111 | helper.title = $('h3', helper.parent);
112 | helper.body = $('div.body', helper.parent);
113 | helper.url = $('div.url', helper.parent);
114 | }
115 |
116 | function settings(element) {
117 | return $.data(element, "tooltip");
118 | }
119 |
120 | // main event handler to start showing tooltips
121 | function handle(event) {
122 | // show helper, either with timeout or on instant
123 | if( settings(this).delay )
124 | tID = setTimeout(show, settings(this).delay);
125 | else
126 | show();
127 |
128 | // if selected, update the helper position when the mouse moves
129 | track = !!settings(this).track;
130 | $(document.body).bind('mousemove', update);
131 |
132 | // update at least once
133 | update(event);
134 | }
135 |
136 | // save elements title before the tooltip is displayed
137 | function save() {
138 | // if this is the current source, or it has no title (occurs with click event), stop
139 | if ( $.tooltip.blocked || this == current || (!this.tooltipText && !settings(this).bodyHandler) )
140 | return;
141 |
142 | // save current
143 | current = this;
144 | title = this.tooltipText;
145 |
146 | if ( settings(this).bodyHandler ) {
147 | helper.title.hide();
148 | var bodyContent = settings(this).bodyHandler.call(this);
149 | if (bodyContent.nodeType || bodyContent.jquery) {
150 | helper.body.empty().append(bodyContent)
151 | } else {
152 | helper.body.html( bodyContent );
153 | }
154 | helper.body.show();
155 | } else if ( settings(this).showBody ) {
156 | var parts = title.split(settings(this).showBody);
157 | helper.title.html(parts.shift()).show();
158 | helper.body.empty();
159 | for(var i = 0, part; (part = parts[i]); i++) {
160 | if(i > 0)
161 | helper.body.append(" ");
162 | helper.body.append(part);
163 | }
164 | helper.body.hideWhenEmpty();
165 | } else {
166 | helper.title.html(title).show();
167 | helper.body.hide();
168 | }
169 |
170 | // if element has href or src, add and show it, otherwise hide it
171 | if( settings(this).showURL && $(this).url() )
172 | helper.url.html( $(this).url().replace('http://', '') ).show();
173 | else
174 | helper.url.hide();
175 |
176 | // add an optional class for this tip
177 | helper.parent.addClass(settings(this).extraClass);
178 |
179 | // fix PNG background for IE
180 | if (settings(this).fixPNG )
181 | helper.parent.fixPNG();
182 |
183 | handle.apply(this, arguments);
184 | }
185 |
186 | // delete timeout and show helper
187 | function show() {
188 | tID = null;
189 | if ((!IE || !$.fn.bgiframe) && settings(current).fade) {
190 | if (helper.parent.is(":animated"))
191 | helper.parent.stop().show().fadeTo(settings(current).fade, current.tOpacity);
192 | else
193 | helper.parent.is(':visible') ? helper.parent.fadeTo(settings(current).fade, current.tOpacity) : helper.parent.fadeIn(settings(current).fade);
194 | } else {
195 | helper.parent.show();
196 | }
197 | update();
198 | }
199 |
200 | /**
201 | * callback for mousemove
202 | * updates the helper position
203 | * removes itself when no current element
204 | */
205 | function update(event) {
206 | if($.tooltip.blocked)
207 | return;
208 |
209 | if (event && event.target.tagName == "OPTION") {
210 | return;
211 | }
212 |
213 | // stop updating when tracking is disabled and the tooltip is visible
214 | if ( !track && helper.parent.is(":visible")) {
215 | $(document.body).unbind('mousemove', update)
216 | }
217 |
218 | // if no current element is available, remove this listener
219 | if( current == null ) {
220 | $(document.body).unbind('mousemove', update);
221 | return;
222 | }
223 |
224 | // remove position helper classes
225 | helper.parent.removeClass("viewport-right").removeClass("viewport-bottom");
226 |
227 | var left = helper.parent[0].offsetLeft;
228 | var top = helper.parent[0].offsetTop;
229 | if (event) {
230 | // position the helper 15 pixel to bottom right, starting from mouse position
231 | left = event.pageX + settings(current).left;
232 | top = event.pageY + settings(current).top;
233 | var right='auto';
234 | if (settings(current).positionLeft) {
235 | right = $(window).width() - left;
236 | left = 'auto';
237 | }
238 | helper.parent.css({
239 | left: left,
240 | right: right,
241 | top: top
242 | });
243 | }
244 |
245 | var v = viewport(),
246 | h = helper.parent[0];
247 | // check horizontal position
248 | if (v.x + v.cx < h.offsetLeft + h.offsetWidth) {
249 | left -= h.offsetWidth + 20 + settings(current).left;
250 | helper.parent.css({left: left + 'px'}).addClass("viewport-right");
251 | }
252 | // check vertical position
253 | if (v.y + v.cy < h.offsetTop + h.offsetHeight) {
254 | top -= h.offsetHeight + 20 + settings(current).top;
255 | helper.parent.css({top: top + 'px'}).addClass("viewport-bottom");
256 | }
257 | }
258 |
259 | function viewport() {
260 | return {
261 | x: $(window).scrollLeft(),
262 | y: $(window).scrollTop(),
263 | cx: $(window).width(),
264 | cy: $(window).height()
265 | };
266 | }
267 |
268 | // hide helper and restore added classes and the title
269 | function hide(event) {
270 | if($.tooltip.blocked)
271 | return;
272 | // clear timeout if possible
273 | if(tID)
274 | clearTimeout(tID);
275 | // no more current element
276 | current = null;
277 |
278 | var tsettings = settings(this);
279 | function complete() {
280 | helper.parent.removeClass( tsettings.extraClass ).hide().css("opacity", "");
281 | }
282 | if ((!IE || !$.fn.bgiframe) && tsettings.fade) {
283 | if (helper.parent.is(':animated'))
284 | helper.parent.stop().fadeTo(tsettings.fade, 0, complete);
285 | else
286 | helper.parent.stop().fadeOut(tsettings.fade, complete);
287 | } else
288 | complete();
289 |
290 | if( settings(this).fixPNG )
291 | helper.parent.unfixPNG();
292 | }
293 |
294 | })(jQuery);
295 |
--------------------------------------------------------------------------------
/schedule/models/calendars.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.contrib.contenttypes import generic
3 | from django.db import models
4 | from django.db.models import Q
5 | from django.contrib.auth.models import User
6 | from django.contrib.contenttypes.models import ContentType
7 | from django.core.urlresolvers import reverse
8 | from django.utils.translation import ugettext, ugettext_lazy as _
9 | from django.template.defaultfilters import slugify
10 | import datetime
11 | from dateutil import rrule
12 | from schedule.utils import EventListManager
13 |
14 | class CalendarManager(models.Manager):
15 | """
16 | >>> user1 = User(username='tony')
17 | >>> user1.save()
18 | """
19 | def get_calendar_for_object(self, obj, distinction=None):
20 | """
21 | This function gets a calendar for an object. It should only return one
22 | calendar. If the object has more than one calendar related to it (or
23 | more than one related to it under a distinction if a distinction is
24 | defined) an AssertionError will be raised. If none are returned it will
25 | raise a DoesNotExistError.
26 |
27 | >>> user = User.objects.get(username='tony')
28 | >>> try:
29 | ... Calendar.objects.get_calendar_for_object(user)
30 | ... except Calendar.DoesNotExist:
31 | ... print "failed"
32 | ...
33 | failed
34 |
35 | Now if we add a calendar it should return the calendar
36 |
37 | >>> calendar = Calendar(name='My Cal')
38 | >>> calendar.save()
39 | >>> calendar.create_relation(user)
40 | >>> Calendar.objects.get_calendar_for_object(user)
41 |
42 |
43 | Now if we add one more calendar it should raise an AssertionError
44 | because there is more than one related to it.
45 |
46 | If you would like to get more than one calendar for an object you should
47 | use get_calendars_for_object (see below).
48 | >>> calendar = Calendar(name='My 2nd Cal')
49 | >>> calendar.save()
50 | >>> calendar.create_relation(user)
51 | >>> try:
52 | ... Calendar.objects.get_calendar_for_object(user)
53 | ... except AssertionError:
54 | ... print "failed"
55 | ...
56 | failed
57 | """
58 | calendar_list = self.get_calendars_for_object(obj, distinction)
59 | if len(calendar_list) == 0:
60 | raise Calendar.DoesNotExist, "Calendar does not exist."
61 | elif len(calendar_list) > 1:
62 | raise AssertionError, "More than one calendars were found."
63 | else:
64 | return calendar_list[0]
65 |
66 | def get_or_create_calendar_for_object(self, obj, distinction = None, name = None):
67 | """
68 | >>> user = User(username="jeremy")
69 | >>> user.save()
70 | >>> calendar = Calendar.objects.get_or_create_calendar_for_object(user, name = "Jeremy's Calendar")
71 | >>> calendar.name
72 | "Jeremy's Calendar"
73 | """
74 | try:
75 | return self.get_calendar_for_object(obj, distinction)
76 | except Calendar.DoesNotExist:
77 | if name is None:
78 | calendar = Calendar(name = unicode(obj))
79 | else:
80 | calendar = Calendar(name = name)
81 | calendar.slug = slugify(calendar.name)
82 | calendar.save()
83 | calendar.create_relation(obj, distinction)
84 | return calendar
85 |
86 | def get_calendars_for_object(self, obj, distinction = None):
87 | """
88 | This function allows you to get calendars for a specific object
89 |
90 | If distinction is set it will filter out any relation that doesnt have
91 | that distinction.
92 | """
93 | ct = ContentType.objects.get_for_model(type(obj))
94 | if distinction:
95 | dist_q = Q(calendarrelation__distinction=distinction)
96 | else:
97 | dist_q = Q()
98 | return self.filter(dist_q, Q(calendarrelation__object_id=obj.id, calendarrelation__content_type=ct))
99 |
100 | class Calendar(models.Model):
101 | '''
102 | This is for grouping events so that batch relations can be made to all
103 | events. An example would be a project calendar.
104 |
105 | name: the name of the calendar
106 | events: all the events contained within the calendar.
107 | >>> calendar = Calendar(name = 'Test Calendar')
108 | >>> calendar.save()
109 | >>> data = {
110 | ... 'title': 'Recent Event',
111 | ... 'start': datetime.datetime(2008, 1, 5, 0, 0),
112 | ... 'end': datetime.datetime(2008, 1, 10, 0, 0)
113 | ... }
114 | >>> event = Event(**data)
115 | >>> event.save()
116 | >>> calendar.events.add(event)
117 | >>> data = {
118 | ... 'title': 'Upcoming Event',
119 | ... 'start': datetime.datetime(2008, 1, 1, 0, 0),
120 | ... 'end': datetime.datetime(2008, 1, 4, 0, 0)
121 | ... }
122 | >>> event = Event(**data)
123 | >>> event.save()
124 | >>> calendar.events.add(event)
125 | >>> data = {
126 | ... 'title': 'Current Event',
127 | ... 'start': datetime.datetime(2008, 1, 3),
128 | ... 'end': datetime.datetime(2008, 1, 6)
129 | ... }
130 | >>> event = Event(**data)
131 | >>> event.save()
132 | >>> calendar.events.add(event)
133 | '''
134 |
135 | name = models.CharField(_("name"), max_length = 200)
136 | slug = models.SlugField(_("slug"),max_length = 200)
137 | objects = CalendarManager()
138 |
139 | class Meta:
140 | verbose_name = _('calendar')
141 | verbose_name_plural = _('calendar')
142 | app_label = 'schedule'
143 |
144 | def __unicode__(self):
145 | return self.name
146 |
147 | def events(self):
148 | return self.event_set.all()
149 | events = property(events)
150 |
151 | def create_relation(self, obj, distinction = None, inheritable = True):
152 | """
153 | Creates a CalendarRelation between self and obj.
154 |
155 | if Inheritable is set to true this relation will cascade to all events
156 | related to this calendar.
157 | """
158 | CalendarRelation.objects.create_relation(self, obj, distinction, inheritable)
159 |
160 | def get_recent(self, amount=5, in_datetime = datetime.datetime.now):
161 | """
162 | This shortcut function allows you to get events that have started
163 | recently.
164 |
165 | amount is the amount of events you want in the queryset. The default is
166 | 5.
167 |
168 | in_datetime is the datetime you want to check against. It defaults to
169 | datetime.datetime.now
170 | """
171 | return self.events.order_by('-start').filter(start__lt=datetime.datetime.now())[:amount]
172 |
173 | def occurrences_after(self, date=None):
174 | return EventListManager(self.events.all()).occurrences_after(date)
175 |
176 | def get_absolute_url(self):
177 | return reverse('calendar_home', kwargs={'calendar_slug':self.slug})
178 |
179 | def add_event_url(self):
180 | return reverse('s_create_event_in_calendar', args=[self.slug])
181 |
182 |
183 | class CalendarRelationManager(models.Manager):
184 | def create_relation(self, calendar, content_object, distinction=None, inheritable=True):
185 | """
186 | Creates a relation between calendar and content_object.
187 | See CalendarRelation for help on distinction and inheritable
188 | """
189 | ct = ContentType.objects.get_for_model(type(content_object))
190 | object_id = content_object.id
191 | cr = CalendarRelation(
192 | content_type = ct,
193 | object_id = object_id,
194 | calendar = calendar,
195 | distinction = distinction,
196 | content_object = content_object
197 | )
198 | cr.save()
199 | return cr
200 |
201 | class CalendarRelation(models.Model):
202 | '''
203 | This is for relating data to a Calendar, and possible all of the events for
204 | that calendar, there is also a distinction, so that the same type or kind of
205 | data can be related in different ways. A good example would be, if you have
206 | calendars that are only visible by certain users, you could create a
207 | relation between calendars and users, with the distinction of 'visibility',
208 | or 'ownership'. If inheritable is set to true, all the events for this
209 | calendar will inherit this relation.
210 |
211 | calendar: a foreign key relation to a Calendar object.
212 | content_type: a foreign key relation to ContentType of the generic object
213 | object_id: the id of the generic object
214 | content_object: the generic foreign key to the generic object
215 | distinction: a string representing a distinction of the relation, User could
216 | have a 'veiwer' relation and an 'owner' relation for example.
217 | inheritable: a boolean that decides if events of the calendar should also
218 | inherit this relation
219 |
220 | DISCLAIMER: while this model is a nice out of the box feature to have, it
221 | may not scale well. If you use this, keep that in mind.
222 | '''
223 |
224 | calendar = models.ForeignKey(Calendar, verbose_name=_("calendar"))
225 | content_type = models.ForeignKey(ContentType)
226 | object_id = models.IntegerField()
227 | content_object = generic.GenericForeignKey('content_type', 'object_id')
228 | distinction = models.CharField(_("distinction"), max_length = 20, null=True)
229 | inheritable = models.BooleanField(_("inheritable"), default=True)
230 |
231 | objects = CalendarRelationManager()
232 |
233 | class Meta:
234 | verbose_name = _('calendar relation')
235 | verbose_name_plural = _('calendar relations')
236 | app_label = 'schedule'
237 |
238 | def __unicode__(self):
239 | return u'%s - %s' %(self.calendar, self.content_object)
240 |
--------------------------------------------------------------------------------
/docs/views.txt:
--------------------------------------------------------------------------------
1 | =====
2 | Views
3 | =====
4 |
5 |
6 | calendar
7 | ========
8 |
9 | This view is for displaying meta_data about calendars. Upcoming events, Name, description and so on and so forth. It should be noted that this is probably not the best view for displaying a calendar in a traditional sense, i.e. displaying a month calendar or a year calendar, as it does not equip the context with any period objects. If you would like to do this you should use calendar_by_period.
10 |
11 | Required Arguments
12 | ------------------
13 |
14 | ``request``
15 | As always the request object
16 |
17 | ``calendar_slug``
18 | The slug of the calendar to be displayed
19 |
20 | Optional Arguments
21 | ------------------
22 |
23 | ``template_name``
24 | default
25 | 'schedule/calendar.html'
26 |
27 | This is the template that will be rendered
28 |
29 | Context Variables
30 | -----------------
31 |
32 | ``calendar``
33 | The Calendar object designated by the ``calendar_slug``.
34 |
35 | calendar_by_period
36 | ==================
37 |
38 | This view is for getting a calendar, but also getting periods with that
39 | calendar. Which periods you get, is designated with the list periods. You
40 | can designate which date you the periods to be initialized to by passing
41 | a date in request.GET. See the template tag ``query_string_for_date``
42 |
43 | Required Arguments
44 | ------------------
45 |
46 | ``request``
47 | As always the request object
48 |
49 | ``calendar_slug``
50 | The slug of the calendar to be displayed
51 |
52 | Optional Arguments
53 | ------------------
54 |
55 | ``template_name``
56 | default
57 | 'schedule/calendar_by_period .html'
58 |
59 | This is the template that will be rendered
60 |
61 | ``periods``
62 | default
63 | ``[]``
64 |
65 | This is a list of Period Subclasses that designates which periods you would like to instantiate and put in the context
66 |
67 | Context Variables
68 | -----------------
69 |
70 | ``date``
71 | This was the date that was generated from the query string.
72 |
73 | ``periods``
74 | this is a dictionary that returns the periods from the list you passed
75 | in. If you passed in Month and Day, then your dictionary would look
76 | like this
77 |
78 | ::
79 |
80 | {
81 | 'month':
82 | 'day':
83 | }
84 |
85 | So in the template to access the Day period in the context you simply
86 | use ``periods.day``.
87 |
88 | ``calendar``
89 | This is the Calendar that is designated by the ``calendar_slug``.
90 |
91 | ``weekday_names``
92 | This is for convenience. It returns the local names of weekedays for
93 | internationalization.
94 |
95 | event
96 | =====
97 |
98 | This view is for showing an event. It is important to remember that an
99 | event is not an occurrence. Events define a set of reccurring occurrences.
100 | If you would like to display an occurrence (a single instance of a
101 | recurring event) use ``occurrence``.
102 |
103 | Required Arguments
104 | ------------------
105 |
106 | ``request``
107 | As always the request object
108 |
109 | ``event_id``
110 | the id of the event to be displayed
111 |
112 | Optional Arguments
113 | ------------------
114 |
115 | ``template_name``
116 | default
117 | 'schedule/calendar_by_period.html'
118 |
119 | This is the template that will be rendered
120 |
121 |
122 |
123 | Context Variables
124 | -----------------
125 |
126 | ``event``
127 | This is the event designated by the event_id
128 |
129 | ``back_url``
130 | this is the url that referred to this view.
131 |
132 | occurrence
133 | ==========
134 |
135 | This view is used to display an occurrence. There are two methods of
136 | displaying an occurrence.
137 |
138 | Required Arguments
139 | ------------------
140 |
141 | ``request``
142 | As always the request object
143 |
144 | ``event_id``
145 | the id of the event that produces the occurrence
146 |
147 | from here you need a way to distinguish the occurrence and that involves
148 |
149 | ``occurrence_id``
150 | if its persisted
151 |
152 | **or** it requires a distinguishing datetime as designated by the keywords below. This should designate the original start date of the occurrence that you wish to access. Using ``get_absolute_url`` from the Occurrence model will help you standardize this.
153 |
154 | * ``year``
155 | * ``month``
156 | * ``day``
157 | * ``hour``
158 | * ``minute``
159 | * ``second``
160 |
161 | Optional Arguments
162 | ------------------
163 |
164 | ``template_name``
165 | default
166 | 'schedule/calendar_by_period.html'
167 |
168 | This is the template that will be rendered
169 |
170 |
171 |
172 | Context Variables
173 | -----------------
174 |
175 | ``event``
176 | the event that produces the occurrence
177 |
178 | ``occurrence``
179 | the occurrence to be displayed
180 |
181 | ``back_url``
182 | the url from which this request was refered
183 |
184 | edit_occurrence
185 | ===============
186 |
187 | This view is used to edit an occurrence.
188 |
189 | Required Arguments
190 | ------------------
191 |
192 | ``request``
193 | As always the request object
194 |
195 | ``event_id``
196 | the id for the event
197 |
198 | from here you need a way to distinguish the occurrence and that involves
199 |
200 | ``occurrence_id``
201 | the id of the occurrence if its persisted
202 |
203 | **or** it requires a distinguishing datetime as designated by the keywords below. This should designate the original start date of the occurrence that you wish to access. Using ``get_edit_url`` from the Occurrence model will help you standardize this.
204 |
205 | * ``year``
206 | * ``month``
207 | * ``day``
208 | * ``hour``
209 | * ``minute``
210 | * ``second``
211 |
212 | Optional Arguments
213 | ------------------
214 |
215 | ``template_name``
216 | default
217 | 'schedule/calendar_by_period.html'
218 |
219 | This is the template that will be rendered
220 |
221 |
222 |
223 | Context Variables
224 | -----------------
225 |
226 | ``form``
227 | an instance of OccurrenceForm to be displayed
228 |
229 | ``occurrence``
230 | an instance of the occurrence being modified
231 |
232 | cancel_occurrence
233 | =================
234 |
235 | This view is used to cancel and occurrence. It is worth noting that canceling an occurrence doesn't stop it from being in occurrence lists or being persisted, it just changes the ``cancelled`` flag on the instance. It is import to check this flag when listing occurrences.
236 |
237 | Also if this view is requested via POST, it will cancel the event and redirect. If this view is accessed via a GET request it will display a confirmation page.
238 |
239 | Required Arguments
240 | ------------------
241 |
242 | ``request``
243 | As always the request object
244 |
245 | from here you need a way to distinguish the occurrence and that involves
246 |
247 | ``occurrence_id``
248 | if its persisted
249 |
250 | **or** it requires a distinguishing datetime as designated by the keywords below. This should designate the original start date of the occurrence that you wish to access. Using get_cancel_url from the Occurrence model will help you standardize this.
251 |
252 | * ``year``
253 | * ``month``
254 | * ``day``
255 | * ``hour``
256 | * ``minute``
257 | * ``second``
258 |
259 | Optional Arguments
260 | ------------------
261 |
262 | ``template_name``
263 | default
264 | 'schedule/calendar_by_period.html'
265 |
266 | This is the template that will be rendered, if this view is accessed via GET
267 |
268 | ``next``
269 | default
270 | the event detail page of ``occurrence.event``
271 |
272 | This is the url you wish to be redirected to after a successful cancelation
273 |
274 | Context Variables
275 | -----------------
276 |
277 | ``occurrence``
278 | an instance of the occurrence being modified
279 |
280 | create_or_edit_event
281 | ====================
282 |
283 | This view is used for creating or editing events. If it receives a GET request or if given an invalid form in a POST request it will render the template, or else it will redirect.
284 |
285 | Required Arguments
286 | ------------------
287 |
288 | ``request``
289 | As always the request object
290 |
291 | ``calendar_id``
292 | This is the calendar id of the event being created or edited.
293 |
294 |
295 | Optional Arguments
296 | ------------------
297 |
298 | ``template_name``
299 | default
300 | 'schedule/calendar_by_period.html'
301 |
302 | This is the template that will be rendered
303 |
304 | ``event_id``
305 | if you are editing an event, you need to pass in the id of the event, so that the form can be pre-propagated with the correct information and also so save works correctly
306 |
307 | ``next``
308 | The url to redirect to upon successful completion or edition.
309 |
310 | Context Variables
311 | -----------------
312 |
313 | ``form``
314 | an instance of EventForm to be displayed.
315 |
316 | ``calendar``
317 | a Calendar with id=calendar_id
318 |
319 | delete_event
320 | ============
321 |
322 | This view is for deleting events. If the view is accessed via a POST request it will delete the event. If it is accessed via a GET request it will render a template to ask for confirmation.
323 |
324 | Required Arguments
325 | ------------------
326 |
327 | ``request``
328 | As always the request object
329 |
330 | ``event_id``
331 | the id of the event to be deleted.
332 |
333 |
334 | Optional Arguments
335 | ------------------
336 |
337 | ``template_name``
338 | default
339 | 'schedule/calendar_by_period.html'
340 |
341 | This is the template that will be rendered
342 |
343 | ``next``
344 | The url to redirect to after successful deletion
345 |
346 | ``login_required``
347 | default
348 | ``True``
349 |
350 | if you want to require a login before deletion happens you can set that here
351 |
352 | Context Variables
353 | -----------------
354 |
355 | ``object``
356 | The event object to be deleted
--------------------------------------------------------------------------------