├── .travis.yml
├── LICENSE
├── README.md
├── reservations
├── __init__.py
├── admin.py
├── forms.py
├── models.py
├── static
│ ├── css
│ │ └── calendar.css
│ └── js
│ │ ├── trans_en-us.coffee
│ │ ├── trans_en-us.min.js
│ │ ├── trans_pl.coffee
│ │ ├── trans_pl.min.js
│ │ ├── ui.coffee
│ │ └── ui.min.js
├── templates
│ ├── calendar.html
│ ├── email_new.html
│ └── forms
│ │ ├── field.html
│ │ └── form.html
├── tests.py
├── urls.py
├── utils.py
└── views.py
├── runtests.py
├── screen1.jpg
└── setup.py
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - 2.7
4 | install:
5 | script:
6 | - pip install -q Django
7 | - python setup.py develop
8 | - ./runtests.py
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | WOW License
2 | =============
3 |
4 | Copyright (c) 2012, Bernard Kobos
5 | No rights reserved.
6 |
7 | Enjoy and do Whatever yOu Want (with it) License
8 |
9 | I grant the persmission for free use to do good things using that software :)
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
12 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
13 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
14 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
15 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
16 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
17 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
18 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
19 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
20 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | django-reservations
2 | ===================
3 |
4 | A simple and customizable Django module for handling reservations.
5 |
6 | 
7 |
8 | Features
9 | --------
10 |
11 | * Customizable reservations (you can provide your own reservation model)
12 | * Configurable (single/multiple reservations per day, default free spots, reservations per month)
13 | * Automatic customizable emails with reservation details
14 | * Custom Django Admin backend
15 | * Ajax calendar for reservation creation and handling
16 | * UI based on [Twitter Bootstrap](http://twitter.github.com/bootstrap/)
17 | * Using i18n to handle translations
18 |
19 | Usage
20 | -----
21 |
22 | You can use django-reseravations as any other django module. You have to add it to your Python PATH. After this add 'reservations' to your INSTALLED_APPS in settings.py. Also you need to set RESERVATION_SPOTS_TOTAL setting so app knows what is the reservations number limit per day. Add it to your urls.py:
23 |
24 | ```python
25 |
26 | urlpatterns = patterns('',
27 | # ...
28 | url(r'^reservations/', include('reservations.urls')),
29 | # ...
30 | )
31 |
32 | ````
33 |
34 | After these basic steps you are ready to go. Just run *manage.py syncdb* to create suitable database models. The reservation app will be available under the URL */reservations/calendar*. Visit it to see how it works and how it looks like ;)
35 |
36 | Customization
37 | ------------
38 |
39 | You can also set some other settings to customize it further:
40 |
41 | RESERVATIONS_PER_MONTH (unlimited by default) - how many reservations can a single user create during one month
42 | RESERVATIONS_PER_DAY (unlimited by default) - how many reservations can a single user create on one day (for example user should not be able to make more than one reservation per day)
43 |
44 | If you would like to add some extra data for each reservation (gender, nationality, whatever..) you can inherit Reservation model and extend it with you custom fields. After that the model will automatically use you new model (and it will update the UI too!). All the Django validation will work too (via ajax!).
45 |
46 | The simplest way is to create a new app inside your project (myreservations for example) and add it to your *INSTALLED_APPS*. In your new app create *models.py* in which you will inherit from Reservation model. After creating your custom model you need to call *update_model* so Resevations module knows about your new model.
47 |
48 | ```python
49 |
50 | from django.db import models
51 | from reservations import update_model
52 | from reservations.models import Reservation
53 |
54 | RACE = (
55 | ('alien', 'Killing Machine'),
56 | ('android', 'Artificial Inteligence'),
57 | ('human', 'Ordinary Guy'),
58 | )
59 |
60 | class DetailedReservation(Reservation):
61 | """Your extra data for the basic model"""
62 | shoe_number = models.IntegerField()
63 | race = models.CharField(max_length=32, choices=RACE)
64 |
65 | def short_desc(self):
66 | """Displayed on the reservation removal button"""
67 | return str(self.id) + "/" + str(self.race)
68 |
69 | update_model(DetailedReservation)
70 |
71 | ```
72 |
73 |
74 | Customizing emails that are sent when reservation is being made can be easily done by creating *email_new.html* template file. Data available in the template is described below. You can check email_new.html template in reservations module for reference.
75 |
76 | ```python
77 |
78 | {'name': username_of_the_user_that_made_a_reservation,
79 | 'date': date_of_the_reservation,
80 | 'reservation_id': reservation_id,
81 | 'extra_data': form_with_extra_data,
82 | 'domain': APP_URL_setting}
83 |
84 | ```
85 |
86 |
87 | Testing
88 | -------
89 |
90 | Project has full unit test coverage of the backend. After adding it to your Django project you can run tests from your shell.
91 |
92 | ./manage.py test reservations
93 |
94 | 
95 |
96 |
97 | TODOs
98 | -----
99 |
100 | * Sample app using Django reservations
101 | * Implemenging user requested features
102 |
103 | Got some questions or suggestions? [Mail me](mailto:bkobos+ghdr@extensa.pl) directly or use the [issue tracker](django-reservations/issues).
--------------------------------------------------------------------------------
/reservations/__init__.py:
--------------------------------------------------------------------------------
1 | from models import SimpleReservation
2 | from forms import TemplatedForm
3 | from django.contrib import admin
4 | # Default reservation model
5 | reservationModel = SimpleReservation
6 |
7 |
8 | class DefaultReservationAdmin(admin.ModelAdmin):
9 | list_display = ('user', 'date')
10 | # list_filter = ('date',)
11 | date_hierarchy = 'date'
12 |
13 |
14 | def get_form():
15 | """Returns templated model form for model that is currently set as reservationModel"""
16 | class ReservationForm(TemplatedForm):
17 | class Meta:
18 | model = reservationModel
19 | # exclude fields from standard Reservation model (show only extra ones in form)
20 | exclude = ('user', 'date', 'created', 'updated', )
21 | return ReservationForm
22 |
23 |
24 | def update_model(newModel, newAdmin=None):
25 | """Update reservationModel variable and update Django admin to include it"""
26 | global reservationModel
27 | reservationModel = newModel
28 | from django.contrib import admin
29 | if not reservationModel in admin.site._registry:
30 | admin.site.register(reservationModel, DefaultReservationAdmin if not newAdmin else newAdmin)
31 |
--------------------------------------------------------------------------------
/reservations/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from models import *
3 |
4 | admin.site.register(ReservationDay)
5 | admin.site.register(Holiday)
6 |
--------------------------------------------------------------------------------
/reservations/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.forms.forms import BoundField
3 | from django.template import Context, loader
4 |
5 |
6 | class TemplatedForm(forms.ModelForm):
7 | '''
8 | From http://djangosnippets.org/snippets/121/
9 | '''
10 | def output_via_template(self):
11 | """Helper function for fieldsting fields data from form"""
12 |
13 | bound_fields = [BoundField(self, field, name) for name, field \
14 | in self.fields.items()]
15 |
16 | c = Context(dict(form=self, bound_fields=bound_fields))
17 | t = loader.get_template('forms/form.html')
18 | return t.render(c)
19 |
20 | def __unicode__(self):
21 | return self.output_via_template()
22 |
--------------------------------------------------------------------------------
/reservations/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class Reservation(models.Model):
6 | """Reservation model - who made a reservation and when"""
7 | user = models.ForeignKey(User)
8 | date = models.DateTimeField(null=False, blank=False)
9 | # Timestamps
10 | created = models.DateTimeField(auto_now_add=True)
11 | updated = models.DateTimeField(auto_now=True, db_index=True)
12 |
13 | class Meta:
14 | abstract = True
15 |
16 | def __unicode__(self):
17 | return str(self.date) + " User: " + str(self.user)
18 |
19 | def short_desc(self):
20 | """Default short description visible on reservation button"""
21 | return str(self.id)
22 |
23 |
24 | class SimpleReservation(Reservation):
25 | """Default non-abstract fallback model used if user does not provide his own implementation"""
26 | pass
27 |
28 |
29 | class ReservationDay(models.Model):
30 | """Reservation day model represeting single day and free/non-free spots for that day"""
31 | date = models.DateField(null=False, blank=False)
32 | spots_total = models.IntegerField(null=True, default=32)
33 | spots_free = models.IntegerField(null=True, default=32)
34 |
35 | def __unicode__(self):
36 | return str(self.date) + " Total: " + str(self.spots_total) + " Free: " + str(self.spots_free)
37 |
38 |
39 | class Holiday(models.Model):
40 | """Define days that are free from work, you don't want to have reservations during holidays"""
41 | name = models.CharField(max_length=100, blank=True)
42 | date = models.DateField(null=False, blank=False)
43 | active = models.BooleanField(default=True, editable=True)
44 | # Timestamps
45 | created = models.DateTimeField(auto_now_add=True)
46 | updated = models.DateTimeField(auto_now=True, db_index=True)
47 |
48 | def __unicode__(self):
49 | return str(self.name) + " " + str(self.date)
50 |
--------------------------------------------------------------------------------
/reservations/static/css/calendar.css:
--------------------------------------------------------------------------------
1 |
2 | /* Body and structure
3 | -------------------------------------------------- */
4 | .year{
5 | font-size:108px;
6 | margin-top:30px;
7 | color: #404040;
8 | margin-bottom: 18px; line-height: 36px;
9 | }
10 | .month{
11 | font-size:42px;
12 | color:red; line-height: 36px;
13 | }
14 | .time{
15 | float:right;
16 | font-size:36px;
17 | text-align:right;
18 | line-height: 36px;
19 | }
20 | .calendar-nav{
21 | list-style: none; margin: 0; padding: 0;
22 | position: absolute; top: 90px; right: 100px;
23 | }
24 | .calendar-nav li{
25 | float: left; margin-right: 12px;
26 | }
27 | .calendar{
28 | height: 580px;
29 | }
30 | .info{
31 | font-size: 0.5em;
32 | }
33 |
34 | .calendar TH{
35 | font-size:13px;
36 | font-weight:bold;
37 | border-top:1px solid #ccc;
38 | border-bottom:1px solid #ccc;
39 | padding: 10px 10px 9px; line-height: 18px;
40 | vertical-align: middle;
41 | }
42 |
43 | .calendar TD{
44 | height:50px;
45 | font-size:24px;
46 | color:#999;
47 |
48 | vertical-align: top; border-top: 1px solid #DDD;
49 | padding: 10px 10px 9px; line-height: 18px; text-align: left;
50 | }
51 | .calendar TD.future:hover{
52 | background-color: #9bfff4;
53 | }
54 | .day-actions{
55 | visibility: hidden;
56 | margin: 10px 0 5px 0; padding: 0; list-style: none;
57 | font-size: 0.5em;
58 | }
59 | .calendar TD.future:hover .day-actions, .calendar TD.reservation:hover .day-actions{
60 | visibility: visible;
61 | }
62 |
63 | .calendar TD.weekend{
64 | color:#CDCDCD;
65 | }
66 |
67 | .calendar TD.today{
68 | background-color:#EFEFEF;
69 | color:#666666;
70 | }
71 |
72 | .calendar TD.reservation{
73 | background-color:#84f66e;
74 | color:#666666;
75 | }
76 |
77 | .calendar TD.past{
78 | background-color:#c6c4c4;
79 | color:#666666;
80 | }
81 |
82 | .calendar TD.holiday{
83 | color:red;
84 | }
85 | .admin-content{
86 | display: block; width: 80%;
87 | margin: 50px 10% 50px 10%;
88 | }
89 | .col1{
90 | float: left; width: 300px;
91 | }
92 | .col2{
93 | float: right; width: 360px;
94 | }
95 | .border-img{
96 | padding: 5px; border: 1px solid #E5E5E5;
97 | }
--------------------------------------------------------------------------------
/reservations/static/js/trans_en-us.coffee:
--------------------------------------------------------------------------------
1 | window.Data =
2 | months: [
3 | "January"
4 | ,"February"
5 | ,"March"
6 | ,"April"
7 | ,"May"
8 | ,"June"
9 | ,"July"
10 | ,"August"
11 | ,"September"
12 | ,"October"
13 | ,"November"
14 | ,"December"
15 | ]
16 |
17 | weekDays: [
18 | "Monday"
19 | ,"Tuesday"
20 | ,"Wednesday"
21 | ,"Thursday"
22 | ,"Friday"
23 | ,"Saturday"
24 | ,"Sunday"
25 | ]
26 |
27 | label:
28 | reserve: "Reserve"
29 | cancel: "Cancel"
30 | txt:
31 | free_spots: "free spots"
32 | operation_forbidden: "Operation forbidden"
33 |
--------------------------------------------------------------------------------
/reservations/static/js/trans_en-us.min.js:
--------------------------------------------------------------------------------
1 |
2 | (function(){window.Data={months:["January","February","March","April","May","June","July","August","September","October","November","December"],weekDays:["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],label:{reserve:"Reserve",cancel:"Cancel"},txt:{free_spots:"free spots",operation_forbidden:"Operation forbidden"}};}).call(this);
--------------------------------------------------------------------------------
/reservations/static/js/trans_pl.coffee:
--------------------------------------------------------------------------------
1 | window.Data =
2 | months: [
3 | "Styczeń"
4 | ,"Luty"
5 | ,"Marzec"
6 | ,"Kwieceń"
7 | ,"Maj"
8 | ,"Czerwiec"
9 | ,"Lipiec"
10 | ,"Sierpień"
11 | ,"Wrzesień"
12 | ,"Październik"
13 | ,"Listopad"
14 | ,"Grudzień"
15 | ]
16 |
17 | weekDays: [
18 | "Poniedziałek"
19 | ,"Wtorek"
20 | ,"Środa"
21 | ,"Czwartek"
22 | ,"Piątek"
23 | ,"Sobota"
24 | ,"Niedziela"
25 | ]
26 |
27 | label:
28 | reserve: "Rezerwuj"
29 | cancel: "Anuluj"
30 | txt:
31 | free_spots: "wolne miejsca"
32 | operation_forbidden: "Operacja niedozwolona"
33 |
--------------------------------------------------------------------------------
/reservations/static/js/trans_pl.min.js:
--------------------------------------------------------------------------------
1 |
2 | (function(){window.Data={months:["Styczeń","Luty","Marzec","Kwieceń","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],weekDays:["Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota","Niedziela"],label:{reserve:"Rezerwuj",cancel:"Anuluj"},txt:{free_spots:"wolne miejsca",operation_forbidden:"Operacja niedozwolona"}};}).call(this);
--------------------------------------------------------------------------------
/reservations/static/js/ui.coffee:
--------------------------------------------------------------------------------
1 | window.App =
2 | monthData: []
3 | defaults: {}
4 |
5 | init: () ->
6 | @calendar = new Calendar($("#calendar"))
7 | new Month(@calendar.month + 1, @calendar.year)
8 | new Reservation(@calendar.year)
9 | new Holidays(@calendar.year)
10 |
11 | renderTime: () ->
12 | now = new Date()
13 | hh = now.getHours();
14 | nn = "0" + now.getMinutes()
15 |
16 | $('.time').html(
17 | hh + ":" + nn.substr(-2)
18 | );
19 |
20 | # Update time
21 | setTimeout(App.renderTime, 500)
22 |
23 |
24 | class AjaxModel
25 | url: "model_url"
26 | constructor: (data=null) ->
27 | $.ajax(
28 | url: @url
29 | data: data
30 | success: @successHandler
31 | error: @errorHandler
32 | )
33 |
34 | successHandler: (data) ->
35 | @data = data
36 |
37 | errorHandler: (data) ->
38 | console.log("ERROR during fetching " + @url)
39 | console.log(data)
40 |
41 |
42 | class Holidays extends AjaxModel
43 | url: "holidays"
44 | constructor: (@year) ->
45 | super(data={year: @year})
46 |
47 | successHandler: (data) ->
48 | @data = data
49 | lengthBefore = App.calendar._holidays.length
50 | for elem in data.holidays
51 | elem.date = new Date(elem.date * 1000)
52 | App.calendar.addHoliday(elem)
53 |
54 | if App.calendar._holidays.length != lengthBefore
55 | # Calendar re-render if something changed
56 | App.calendar.render()
57 |
58 | class Month
59 | data: {}
60 | constructor: (@month, @year) ->
61 | # Retrive month data from server
62 | that = @
63 | $.ajax(
64 | url: 'month/' + @month + '/' + @year
65 | success: (data) -> that.successHandler.call(that, data)
66 | error: @errorHandler
67 | )
68 |
69 | successHandler: (data) ->
70 | for elem in data
71 | date = new Date(elem.fields.date * 1000)
72 | date.setHours(0)
73 | App.monthData[date] = elem.fields
74 | App.calendar.render()
75 |
76 | errorHandler: (data) ->
77 | console.log("ERROR during fetching user data!")
78 | console.log(data)
79 |
80 | class Reservation
81 | constructor: (@year) ->
82 | # Rertive user reservations for year
83 | $.ajax(
84 | url: 'reservation'
85 | data:
86 | year: @year
87 | success: @successHandler
88 | error: @errorHandler
89 | )
90 |
91 | successHandler: (data) ->
92 | @data = data
93 | lengthBefore = App.calendar._reservations.length
94 | for elem in data.reservations
95 | elem.date = new Date(elem.date * 1000)
96 | App.calendar.addReservation(elem)
97 |
98 | if App.calendar._reservations.length != lengthBefore
99 | # Calendar re-render if something changed
100 | App.calendar.render()
101 |
102 | errorHandler: (data) ->
103 | console.log("ERROR during fetching user reservations!")
104 | console.log(data)
105 |
106 | class Calendar
107 | constructor: (@elem) ->
108 | @tableHeader = @elem.find('> thead:last')
109 | @tableBody = @elem.find('> tbody:last')
110 | now = new Date()
111 | @month = now.getMonth()
112 | @year = now.getFullYear()
113 | @modalInfo = $('#modal-info')
114 | @modalDetail = $('#modal-detail-form')
115 |
116 | updateReservationDay: (reservationDay) ->
117 | date = new Date(reservationDay.fields.date * 1000)
118 | date.setHours(0)
119 | App.monthData[date] = reservationDay.fields
120 |
121 | _reservations: []
122 | _holidays: []
123 |
124 | addReservation: (date) ->
125 | @_reservations.push(date)
126 |
127 | addHoliday: (holiday) ->
128 | @_holidays.push(holiday)
129 |
130 | removeReservation: (reservation_id) ->
131 | index = 0
132 | while index < @_reservations.length
133 | remove = (@_reservations[index].id == reservation_id)
134 | if remove
135 | @_reservations.splice(index, 1)
136 | else
137 | index += 1
138 |
139 | inReservations: (day, month, year) ->
140 | for elem in @_reservations
141 | if elem.date.getDate() == day and elem.date.getMonth() == month and elem.date.getFullYear() == year
142 | return true
143 | return false
144 |
145 | getReservations: (day, month, year) ->
146 | out = []
147 | for elem in @_reservations
148 | if elem.date.getDate() == day and elem.date.getMonth() == month and elem.date.getFullYear() == year
149 | out.push(elem)
150 | return out
151 |
152 | daysInMonth: (month, year) ->
153 | return new Date(year, month, 0).getDate()
154 |
155 | makeReservation: (mon, dayOfMonth, extraData=null) ->
156 | that = @
157 | data =
158 | year: mon.getFullYear()
159 | month: mon.getMonth() + 1
160 | day: dayOfMonth
161 | csrfmiddlewaretoken: $("input[name=csrfmiddlewaretoken]").val()
162 | $.ajax(
163 | url: 'reservation'
164 | type: 'POST'
165 | data: $.param(data) + if extraData then "&" + extraData else ""
166 | success: (data) -> that.reservationSuccess.call(that, data)
167 | error: (data) -> that.reservationError.call(that, data)
168 | )
169 |
170 | renderDays: (mon) ->
171 | # Clear
172 | @tableBody.empty()
173 | # Get some important days
174 | now = new Date()
175 | fdom = mon.getDay() - 1 # First day of month
176 | if fdom < 0
177 | fdom = 7 + fdom
178 |
179 | mwks = 6 # Weeks in month
180 | # Render days
181 | dayOfMonth = 0
182 | first = 0
183 | last = 0
184 | i = 0
185 | daysInMonth = @daysInMonth(mon.getMonth() + 1, mon.getFullYear())
186 | while i >= last
187 | _html = ""
188 | for weekDay in [0...Data.weekDays.length]
189 | divClass = []
190 | message = ""
191 | id = ""
192 | currdate = new Date(mon.getFullYear(), mon.getMonth(), dayOfMonth + 1)
193 | # Determine if we have reached the first day of the month
194 | if first >= daysInMonth
195 | dayOfMonth = 0
196 | else if (dayOfMonth > 0 and first > 0) or weekDay == fdom
197 | dayOfMonth += 1
198 | first +=1
199 |
200 | # Get last day of month
201 | if 1 * dayOfMonth == daysInMonth
202 | last = daysInMonth
203 |
204 | # Check Holidays schedule
205 | for holiday in @_holidays
206 | if holiday.date.getTime() == currdate.getTime()
207 | divClass.push("holiday")
208 | message = holiday.name
209 |
210 | # Set class
211 | in_future = () ->
212 | if (dayOfMonth > now.getDate() and \
213 | now.getMonth() == mon.getMonth() and \
214 | now.getFullYear() == mon.getFullYear() ) or \
215 | now.getMonth() < mon.getMonth() or \
216 | now.getFullYear() < mon.getFullYear()
217 | return true
218 | return false
219 |
220 | if divClass.length == 0
221 | if @inReservations(dayOfMonth, mon.getMonth(), mon.getFullYear())
222 | divClass.push("reservation")
223 | if dayOfMonth == now.getDate() and \
224 | now.getMonth() == mon.getMonth() and \
225 | now.getFullYear() == mon.getFullYear()
226 | divClass.push("today")
227 | else if weekDay in [5, 6]
228 | divClass.push("weekend")
229 | else if in_future()
230 | if not App.monthData[currdate] or App.monthData[currdate].spots_free > 0
231 | divClass.push("future")
232 | else
233 | divClass.push("past")
234 |
235 | # Set ID
236 | id = "cell_" + i + "" + weekDay + "" + (if dayOfMonth > 10 then dayOfMonth else "0" + dayOfMonth)
237 |
238 | # Render HTML
239 | if dayOfMonth == 0
240 | _html += '
'
241 | else if message.length > 0
242 | _html += '' + dayOfMonth
243 | _html += ''+message+' '
244 | else
245 | _html += '' + dayOfMonth
246 | dayReservations = @getReservations(dayOfMonth, mon.getMonth(), mon.getFullYear())
247 | if "future" in divClass and (not App.defaults.reservations_limit or dayReservations.length < App.defaults.reservations_limit)
248 | _html += '' + Data.label.reserve + ' '
249 | if "reservation" in divClass and in_future()
250 | for reservation in dayReservations
251 | _html += '[' + reservation.short_desc + "] " + Data.label.cancel + ' '
252 |
253 | if ("reservation" in divClass or "future" in divClass) and in_future()
254 | if App.monthData[currdate]
255 | spots_free = App.monthData[currdate].spots_free
256 | else
257 | spots_free = App.defaults.spots_free
258 | _html += '' + spots_free + ' ' + Data.txt.free_spots + '
'
259 |
260 |
261 |
262 | _html = "" +_html+ " ";
263 | @tableBody.append(_html);
264 | that = @
265 | $('.btn-reserve').unbind('click').bind('click', () ->
266 | # Get day from ID
267 | dayOfMonth = $(this).closest("td").attr("id").substr(-2)
268 | # Do we need to collect extra data from user?
269 | if App.defaults.get_extra_data
270 | that.clearErrors('')
271 | that.modalDetail.modal()
272 | $('.btn-primary', that.modalDetail).unbind('click').bind('click', () ->
273 | that.makeReservation(mon, dayOfMonth, $("form", that.modalDetail).serialize())
274 | )
275 | else
276 | that.makeReservation(mon, dayOfMonth)
277 | )
278 |
279 | $('.btn-unreserve').unbind('click').bind('click', () ->
280 | # Get day from ID
281 | dayOfMonth = $(this).closest("td").attr("id").substr(-2)
282 |
283 | params =
284 | id: $(this).attr("db-id")
285 | $.ajax(
286 | url: 'reservation?' + $.param(params)
287 | type: 'DELETE'
288 | headers:
289 | "X-CSRFToken": $("input[name=csrfmiddlewaretoken]").val()
290 | success: (data) -> that.unreservationSuccess.call(that, data)
291 | error: (data) -> that.unreservationError.call(that, data)
292 | )
293 | )
294 | i += 1
295 |
296 | clearErrors: (defaultClass="success") ->
297 | # Clear old errors
298 | for element in document.getElementsByClassName("control-group")
299 | element.classList.remove("error")
300 | if defaultClass
301 | element.classList.add(defaultClass)
302 | for element in document.getElementsByClassName("help-inline")
303 | element.innerHTML = ""
304 |
305 | renderError: (fieldName, text) ->
306 | # Find the field
307 | field = document.getElementById("id_" + fieldName)
308 | # Check field is already wrapped with Bootstrap DOM structure
309 | container = field.parentNode.parentNode
310 | error_desc = container.getElementsByClassName("help-inline")[0]
311 | error_desc.innerHTML = text
312 | container.classList.remove("success")
313 | container.classList.add("error")
314 |
315 | reservationSuccess: (data) ->
316 | # Detailed reservation validation errors
317 | if "errors" of data
318 | @clearErrors()
319 | for error in data.errors
320 | @renderError(error[0], error[1])
321 | else
322 | # Update reservation day and add reservation
323 | @updateReservationDay(data.reservation_day)
324 | reservation = data.reservation
325 | reservation.date = new Date(reservation.date * 1000)
326 | @addReservation(reservation)
327 | # Close modal if is being used
328 | @modalDetail.modal('hide')
329 | # Repaint
330 | App.calendar.render()
331 |
332 | reservationError: (data) ->
333 | # Show alert with message
334 | $('h3', @modalInfo).text(Data.txt.operation_forbidden)
335 | $('p', @modalInfo).text(data.responseText)
336 | @modalInfo.modal('show')
337 |
338 | unreservationSuccess: (data) ->
339 | # Update reservation day and remove reservation
340 | @updateReservationDay(data.reservation_day)
341 | @removeReservation(data.id)
342 | # Repaint
343 | App.calendar.render()
344 |
345 | unreservationError: (data) ->
346 | # Show alert with message
347 | $('h3', @modalInfo).text(Data.txt.operation_forbidden)
348 | $('p', @modalInfo).text(data.responseText)
349 | @modalInfo.modal('show')
350 |
351 | renderDaysOfWeek: () ->
352 | # Clear first
353 | @tableHeader.empty()
354 | # Render Days of Week
355 | for j in [0...Data.weekDays.length]
356 | _html += "" + Data.weekDays[j] + " "
357 | _html = "" + _html + " "
358 | @tableHeader.append(_html)
359 |
360 | # Render whole calendar
361 | render: (mm=null, yy=null) ->
362 | now = new Date()
363 | # Default (now)
364 | if mm != null and yy != null
365 | @month = mm
366 | @year = yy
367 |
368 | mm = @month
369 | yy = @year
370 |
371 | # create viewed date object
372 | mon = new Date(yy, mm, 1)
373 | yp = mon.getFullYear()
374 | yn = mon.getFullYear()
375 |
376 | $('#last').removeClass('disabled')
377 | if now.getMonth() > mm-1 and now.getFullYear() == yy
378 | $('#last').addClass('disabled')
379 |
380 | prv = new Date(yp, mm - 1, 1)
381 | nxt = new Date(yn, mm + 1, 1)
382 |
383 | # Render Month
384 | $('.year').html(mon.getFullYear())
385 | $('.month').html(Data.months[mon.getMonth()])
386 |
387 | # Clear view
388 | @renderDaysOfWeek()
389 | @renderDays(mon)
390 |
391 |
392 | $('#last').unbind('click').bind('click', () ->
393 | if not $(this).hasClass("disabled")
394 | App.calendar.render(prv.getMonth(), prv.getFullYear())
395 | new Month(prv.getMonth() + 1, prv.getFullYear())
396 | )
397 |
398 | $('#current').unbind('click').bind('click', () ->
399 | App.calendar.render(now.getMonth(), now.getFullYear())
400 | )
401 |
402 | $('#next').unbind('click').bind('click', () ->
403 | App.calendar.render(nxt.getMonth(), nxt.getFullYear())
404 | new Month(nxt.getMonth() + 1, nxt.getFullYear())
405 | )
406 |
407 | # Load
408 | $(document).ready(() ->
409 | # Initialize
410 | App.init()
411 | # Render the calendar
412 | App.calendar.render()
413 | App.renderTime()
414 | )
415 |
--------------------------------------------------------------------------------
/reservations/static/js/ui.min.js:
--------------------------------------------------------------------------------
1 |
2 | (function(){var AjaxModel,Calendar,Holidays,Month,Reservation,__hasProp={}.hasOwnProperty,__extends=function(child,parent){for(var key in parent){if(__hasProp.call(parent,key))child[key]=parent[key];}function ctor(){this.constructor=child;}ctor.prototype=parent.prototype;child.prototype=new ctor();child.__super__=parent.prototype;return child;},__indexOf=[].indexOf||function(item){for(var i=0,l=this.length;i thead:last');this.tableBody=this.elem.find('> tbody:last');now=new Date();this.month=now.getMonth();this.year=now.getFullYear();this.modalInfo=$('#modal-info');this.modalDetail=$('#modal-detail-form');}
11 | Calendar.prototype.updateReservationDay=function(reservationDay){var date;date=new Date(reservationDay.fields.date*1000);date.setHours(0);return App.monthData[date]=reservationDay.fields;};Calendar.prototype._reservations=[];Calendar.prototype._holidays=[];Calendar.prototype.addReservation=function(date){return this._reservations.push(date);};Calendar.prototype.addHoliday=function(holiday){return this._holidays.push(holiday);};Calendar.prototype.removeReservation=function(reservation_id){var index,remove,_results;index=0;_results=[];while(index=last){_html="";for(weekDay=_i=0,_ref=Data.weekDays.length;0<=_ref?_i<_ref:_i>_ref;weekDay=0<=_ref?++_i:--_i){divClass=[];message="";id="";currdate=new Date(mon.getFullYear(),mon.getMonth(),dayOfMonth+1);if(first>=daysInMonth){dayOfMonth=0;}else if((dayOfMonth>0&&first>0)||weekDay===fdom){dayOfMonth+=1;first+=1;}
17 | if(1*dayOfMonth===daysInMonth){last=daysInMonth;}
18 | _ref1=this._holidays;for(_j=0,_len=_ref1.length;_j<_len;_j++){holiday=_ref1[_j];if(holiday.date.getTime()===currdate.getTime()){divClass.push("holiday");message=holiday.name;}}
19 | in_future=function(){if((dayOfMonth>now.getDate()&&now.getMonth()===mon.getMonth()&&now.getFullYear()===mon.getFullYear())||now.getMonth()0){divClass.push("future");}}else{divClass.push("past");}}
22 | id="cell_"+i+""+weekDay+""+(dayOfMonth>10?dayOfMonth:"0"+dayOfMonth);if(dayOfMonth===0){_html+=' ';}else if(message.length>0){_html+=''+dayOfMonth;_html+=''+message+' ';}else{_html+=''+dayOfMonth;dayReservations=this.getReservations(dayOfMonth,mon.getMonth(),mon.getFullYear());if(__indexOf.call(divClass,"future")>=0&&(!App.defaults.reservations_limit||dayReservations.length ';}
23 | if(__indexOf.call(divClass,"reservation")>=0&&in_future()){for(_k=0,_len1=dayReservations.length;_k<_len1;_k++){reservation=dayReservations[_k];_html+='['+reservation.short_desc+"] "+Data.label.cancel+' ';}}
24 | if((__indexOf.call(divClass,"reservation")>=0||__indexOf.call(divClass,"future")>=0)&&in_future()){if(App.monthData[currdate]){spots_free=App.monthData[currdate].spots_free;}else{spots_free=App.defaults.spots_free;}
25 | _html+=''+spots_free+' '+Data.txt.free_spots+'
';}}}
26 | _html=""+_html+" ";this.tableBody.append(_html);that=this;$('.btn-reserve').unbind('click').bind('click',function(){dayOfMonth=$(this).closest("td").attr("id").substr(-2);if(App.defaults.get_extra_data){that.clearErrors('');that.modalDetail.modal();return $('.btn-primary',that.modalDetail).unbind('click').bind('click',function(){return that.makeReservation(mon,dayOfMonth,$("form",that.modalDetail).serialize());});}else{return that.makeReservation(mon,dayOfMonth);}});$('.btn-unreserve').unbind('click').bind('click',function(){var params;dayOfMonth=$(this).closest("td").attr("id").substr(-2);params={id:$(this).attr("db-id")};return $.ajax({url:'reservation?'+$.param(params),type:'DELETE',headers:{"X-CSRFToken":$("input[name=csrfmiddlewaretoken]").val()},success:function(data){return that.unreservationSuccess.call(that,data);},error:function(data){return that.unreservationError.call(that,data);}});});_results.push(i+=1);}
27 | return _results;};Calendar.prototype.clearErrors=function(defaultClass){var element,_i,_j,_len,_len1,_ref,_ref1,_results;if(defaultClass==null){defaultClass="success";}
28 | _ref=document.getElementsByClassName("control-group");for(_i=0,_len=_ref.length;_i<_len;_i++){element=_ref[_i];element.classList.remove("error");if(defaultClass){element.classList.add(defaultClass);}}
29 | _ref1=document.getElementsByClassName("help-inline");_results=[];for(_j=0,_len1=_ref1.length;_j<_len1;_j++){element=_ref1[_j];_results.push(element.innerHTML="");}
30 | return _results;};Calendar.prototype.renderError=function(fieldName,text){var container,error_desc,field;field=document.getElementById("id_"+fieldName);container=field.parentNode.parentNode;error_desc=container.getElementsByClassName("help-inline")[0];error_desc.innerHTML=text;container.classList.remove("success");return container.classList.add("error");};Calendar.prototype.reservationSuccess=function(data){var error,reservation,_i,_len,_ref,_results;if("errors"in data){this.clearErrors();_ref=data.errors;_results=[];for(_i=0,_len=_ref.length;_i<_len;_i++){error=_ref[_i];_results.push(this.renderError(error[0],error[1]));}
31 | return _results;}else{this.updateReservationDay(data.reservation_day);reservation=data.reservation;reservation.date=new Date(reservation.date*1000);this.addReservation(reservation);this.modalDetail.modal('hide');return App.calendar.render();}};Calendar.prototype.reservationError=function(data){$('h3',this.modalInfo).text(Data.txt.operation_forbidden);$('p',this.modalInfo).text(data.responseText);return this.modalInfo.modal('show');};Calendar.prototype.unreservationSuccess=function(data){this.updateReservationDay(data.reservation_day);this.removeReservation(data.id);return App.calendar.render();};Calendar.prototype.unreservationError=function(data){$('h3',this.modalInfo).text(Data.txt.operation_forbidden);$('p',this.modalInfo).text(data.responseText);return this.modalInfo.modal('show');};Calendar.prototype.renderDaysOfWeek=function(){var j,_html,_i,_ref;this.tableHeader.empty();for(j=_i=0,_ref=Data.weekDays.length;0<=_ref?_i<_ref:_i>_ref;j=0<=_ref?++_i:--_i){_html+=""+Data.weekDays[j]+" ";}
32 | _html=""+_html+" ";return this.tableHeader.append(_html);};Calendar.prototype.render=function(mm,yy){var mon,now,nxt,prv,yn,yp;if(mm==null){mm=null;}
33 | if(yy==null){yy=null;}
34 | now=new Date();if(mm!==null&&yy!==null){this.month=mm;this.year=yy;}
35 | mm=this.month;yy=this.year;mon=new Date(yy,mm,1);yp=mon.getFullYear();yn=mon.getFullYear();$('#last').removeClass('disabled');if(now.getMonth()>mm-1&&now.getFullYear()===yy){$('#last').addClass('disabled');}
36 | prv=new Date(yp,mm-1,1);nxt=new Date(yn,mm+1,1);$('.year').html(mon.getFullYear());$('.month').html(Data.months[mon.getMonth()]);this.renderDaysOfWeek();this.renderDays(mon);$('#last').unbind('click').bind('click',function(){if(!$(this).hasClass("disabled")){App.calendar.render(prv.getMonth(),prv.getFullYear());return new Month(prv.getMonth()+1,prv.getFullYear());}});$('#current').unbind('click').bind('click',function(){return App.calendar.render(now.getMonth(),now.getFullYear());});return $('#next').unbind('click').bind('click',function(){App.calendar.render(nxt.getMonth(),nxt.getFullYear());return new Month(nxt.getMonth()+1,nxt.getFullYear());});};return Calendar;})();$(document).ready(function(){App.init();App.calendar.render();return App.renderTime();});}).call(this);
--------------------------------------------------------------------------------
/reservations/templates/calendar.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% block extrahead %}
4 |
5 |
6 |
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
34 |
35 |
36 |
37 |
38 |
39 |
52 |
53 |
65 |
66 |
67 |
74 | {% endblock %}
--------------------------------------------------------------------------------
/reservations/templates/email_new.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {% trans "Welcome" %} {{name}},
20 |
21 |
22 | {% trans "Your reservation" %} (ID: {{reservation_id}} ) {%trans "for" %} {{ date|date:"d-m-Y"}} {% trans "has been successfully created" %}.
23 | {% if extra_data %}
24 |
25 | {% for name, val in extra_data.cleaned_data.items %}
26 | {{ name }}: {{val}}
27 | {% endfor %}
28 | {% endif %}
29 |
30 | {% trans "Remember! You can cancel your reservation and change the reservation day no later than 48 hours before the selected term" %}. {% trans "To make the change, log in to" %} {% trans "E-reservation system" %} {% trans "and make the change" %}.
31 |
32 |
33 | Thanks,
34 | Django Reservaton System
35 |
36 |
37 |
--------------------------------------------------------------------------------
/reservations/templates/forms/field.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 |
3 | {% endcomment %}
4 |
5 |
{{ field.label }}{% if field.field.required %}* {% endif %}:
6 |
{{ field }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/reservations/templates/forms/form.html:
--------------------------------------------------------------------------------
1 | {% for field in bound_fields %}
2 | {% include "forms/field.html" %}
3 | {% endfor %}
--------------------------------------------------------------------------------
/reservations/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from models import Reservation, SimpleReservation, Holiday
3 | from django.core.urlresolvers import reverse
4 | from django.test.client import Client
5 | from django.test.utils import override_settings
6 | # from django.conf import settings
7 | import json
8 | from django.db import models
9 | import datetime as dt
10 | from datetime import datetime
11 | from . import update_model
12 |
13 |
14 | class ExtendedReservation(Reservation):
15 | """Some extra data for the basic model"""
16 | extra_data_required = models.CharField(max_length=100)
17 |
18 |
19 | class TestLoggedIn(TestCase):
20 | code = "foo"
21 | reservtion_data = {
22 | "simple": {"year": 2032, "month": 12, "day": 12},
23 | }
24 |
25 | def setUp(self):
26 | # Create test user in DB
27 | from django.contrib.auth.models import User
28 | user = User.objects.create_user('fred', 'fred@astor.com', 'astor')
29 | user.save()
30 | self.client = Client()
31 | self.client.login(username='fred', password='astor')
32 |
33 | # from datetime import datetime
34 | self.reservtion_data['late'] = {"year": datetime.now().year,
35 | "month": datetime.now().month,
36 | "day": datetime.now().day}
37 | # By default use the default SimpleReservation model
38 | update_model(SimpleReservation)
39 |
40 | def test_not_authorized(self):
41 | """Not logged in user tries to make a reservation"""
42 | self.client.logout()
43 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=False)
44 | self.assertTrue('/accounts/login/' in response['Location'])
45 |
46 | @override_settings(RESERVATIONS_PER_DAY=2)
47 | def test_above_threshold(self):
48 | """User should not be able to make more than RESERVATIONS_PER_DAY reservation"""
49 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=True)
50 | # print "RESPONSE", response.content
51 | # print "response.redirect_chain", response.redirect_chain
52 | self.assertEqual(response.status_code, 200)
53 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=True)
54 | self.assertEqual(response.status_code, 200)
55 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=True)
56 | # print "Number of reservations", SimpleReservation.objects.all().count(), reverse('reservations_reservation')
57 | self.assertEqual(response.status_code, 403)
58 | self.assertEqual(SimpleReservation.objects.all().count(), 2)
59 |
60 | def test_reservation(self):
61 | """Successfully create reservation"""
62 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=True)
63 | self.assertEqual(response.status_code, 200)
64 | self.assertEqual(SimpleReservation.objects.all().count(), 1)
65 |
66 | def test_cancel_reservation(self):
67 | """Test cancallation of reservation"""
68 | # First, create a reservation
69 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=True)
70 | self.assertEqual(response.status_code, 200)
71 | self.assertEqual(SimpleReservation.objects.all().count(), 1)
72 | reservation_id = json.loads(response.content)['reservation']["id"]
73 | # Then, cancel it
74 | response = self.client.delete(reverse('reservations_reservation'), {"id": reservation_id}, follow=True)
75 | self.assertEqual(response.status_code, 200)
76 | self.assertEqual(SimpleReservation.objects.all().count(), 0)
77 | # Create reservation for less than 48h in future
78 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['late'], follow=True)
79 | self.assertEqual(response.status_code, 200)
80 | self.assertEqual(SimpleReservation.objects.all().count(), 1)
81 | reservation_id = json.loads(response.content)['reservation']["id"]
82 | # Try to cancel it (error!)
83 | response = self.client.delete(reverse('reservations_reservation'), {"id": reservation_id}, follow=True)
84 | self.assertEqual(response.status_code, 403)
85 | self.assertEqual(SimpleReservation.objects.all().count(), 1)
86 |
87 | def test_extra_data_form(self):
88 | """User should not be able to make a reservation without required fields"""
89 | update_model(ExtendedReservation)
90 | response = self.client.post(reverse('reservations_reservation'), self.reservtion_data['simple'], follow=True)
91 | self.assertTrue("errors" in response.content)
92 | self.assertEqual(ExtendedReservation.objects.all().count(), 0)
93 | # Provide some extra data
94 | extendedData = self.reservtion_data['simple'].copy()
95 | extendedData['extra_data_required'] = "foo"
96 | response = self.client.post(reverse('reservations_reservation'), extendedData, follow=True)
97 | self.assertFalse("errors" in response.content)
98 | self.assertEqual(ExtendedReservation.objects.all().count(), 1)
99 |
100 | def test_holiday(self):
101 | """User should not be able to make a reservation on holiday day"""
102 | # Create a holiday
103 | holiday = Holiday(name="Test Holiday", active=True, date=dt.date(2032, 12, 13))
104 | holiday.save()
105 | # Try to make a reservation
106 | holiday_date = {"year": 2032, "month": 12, "day": 13}
107 | response = self.client.post(reverse('reservations_reservation'), holiday_date, follow=True)
108 | self.assertEqual(response.status_code, 403)
109 | self.assertEqual(SimpleReservation.objects.all().count(), 0)
110 | # Disable holiday and re-try to make a reservation
111 | holiday.active = False
112 | holiday.save()
113 | response = self.client.post(reverse('reservations_reservation'), holiday_date, follow=True)
114 | self.assertEqual(response.status_code, 200)
115 | self.assertEqual(SimpleReservation.objects.all().count(), 1)
116 |
--------------------------------------------------------------------------------
/reservations/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 | from django.views.generic.simple import *
3 | from views import *
4 | # Enable admin
5 | from django.contrib import admin
6 | admin.autodiscover()
7 |
8 | urlpatterns = patterns('',
9 | (r'^month/(?P\d*)/(?P\d*)', MonthDetailView.as_view()),
10 | url(r'^reservation$', Reservation.as_view(), name='reservations_reservation'),
11 | url(r'^calendar$', calendar_view, name='reservations_calendar'),
12 | url(r'^holidays$', get_holidays, name='reservations_holidays'),
13 | )
14 |
--------------------------------------------------------------------------------
/reservations/utils.py:
--------------------------------------------------------------------------------
1 | from django.template.loader import render_to_string
2 | from django.core.mail import EmailMessage
3 | from django.conf import settings
4 |
5 |
6 | def send_email(email_to, title, template, data):
7 | html_content = render_to_string(template, data)
8 | msg = EmailMessage(title, html_content, settings.EMAIL_FROM, [email_to])
9 | msg.content_subtype = "html" # Main content is now text/html
10 | msg.send()
11 |
--------------------------------------------------------------------------------
/reservations/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render_to_response
2 | from django.contrib.auth.decorators import login_required
3 | from django.utils.decorators import method_decorator
4 | from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
5 | from django.template import RequestContext
6 | from django.conf import settings
7 | import time
8 | import datetime
9 | from django.db import models
10 | from django.utils.simplejson import JSONEncoder
11 | from django.core.serializers import serialize
12 | from django.db.models.query import QuerySet
13 | from django.forms.models import model_to_dict
14 | from models import *
15 | from django.db.models import F
16 | from django.utils.translation import ugettext as _
17 | from . import get_form, reservationModel
18 | from django import http
19 | from django.utils import simplejson as json
20 | from django.views.generic import View
21 | import calendar
22 | from utils import send_email
23 |
24 |
25 | class DjangoJSONEncoder(JSONEncoder):
26 | def default(self, obj):
27 | if isinstance(obj, QuerySet):
28 | # `default` must return a python serializable
29 | # structure, the easiest way is to load the JSON
30 | # string produced by `serialize` and return it
31 | return serialize('python', obj)
32 | if isinstance(obj, models.Model):
33 | # do the same as above by making it a queryset first
34 | set_obj = [obj]
35 | set_str = serialize('python', set_obj)
36 | return set_str[0]
37 | if isinstance(obj, datetime.datetime):
38 | return time.mktime(obj.timetuple())
39 | if isinstance(obj, datetime.date):
40 | return time.mktime(obj.timetuple())
41 | return JSONEncoder.default(self, obj)
42 |
43 |
44 | # From https://docs.djangoproject.com/en/1.4/topics/class-based-views/#more-than-just-html
45 | class JSONResponseMixin(object):
46 | def render_to_response(self, context):
47 | "Returns a JSON response containing 'context' as payload"
48 | return self.get_json_response(self.convert_context_to_json(context))
49 |
50 | def get_json_response(self, content, **httpresponse_kwargs):
51 | "Construct an `HttpResponse` object."
52 | return http.HttpResponse(content,
53 | content_type='application/json',
54 | **httpresponse_kwargs)
55 |
56 | def convert_context_to_json(self, context):
57 | "Convert the context dictionary into a JSON object"
58 | # Note: This is *EXTREMELY* naive; in reality, you'll need
59 | # to do much more complex handling to ensure that arbitrary
60 | # objects -- such as Django model instances or querysets
61 | # -- can be serialized as JSON.
62 | return json.dumps(context, cls=DjangoJSONEncoder)
63 |
64 |
65 | class Reservation(JSONResponseMixin, View):
66 | @method_decorator(login_required)
67 | def post(self, request):
68 | year = int(request.POST['year'])
69 | month = int(request.POST['month'])
70 | day = int(request.POST['day'])
71 | date = datetime.date(year, month, day)
72 |
73 | # If user is using custom form, validate it
74 | form = get_form()(request.POST, initial={'user': request.user, 'date': date})
75 | if not form.is_valid():
76 | return self.render_to_response({'success': False,
77 | 'errors': [(k, v[0]) for k, v in form.errors.items()]})
78 |
79 | # Check if it is not a holiday day
80 | if Holiday.objects.filter(date=datetime.date(year, month, day), active=True).count() != 0:
81 | return HttpResponseForbidden(_("You can not make reservations on holidays"))
82 |
83 | # Check if user can create new reservation in selected month
84 | # If reservations_per_month setting is set
85 | first, days_month = calendar.monthrange(year, month)
86 | if hasattr(settings, 'RESERVATIONS_PER_MONTH') and \
87 | reservationModel.objects.filter(user=request.user,
88 | date__gte=datetime.date(year, month, 1),
89 | date__lte=datetime.date(year, month, days_month)).count() >= settings.RESERVATIONS_PER_MONTH:
90 | return HttpResponseForbidden(_("You have already made a reservation during that month"))
91 |
92 | if hasattr(settings, 'RESERVATIONS_PER_DAY') and \
93 | reservationModel.objects.filter(user=request.user,
94 | date=datetime.date(year, month, day)).count() >= settings.RESERVATIONS_PER_DAY:
95 | return HttpResponseForbidden(_("You have already made a reservation during that day"))
96 |
97 | if not hasattr(settings, 'RESERVATION_SPOTS_TOTAL'):
98 | raise Exception("Critical error. Setting not set, contact admin")
99 |
100 | # Check if spot is still available
101 | reservation_day = ReservationDay.objects.get_or_create(date=date,
102 | defaults={'spots_free': settings.RESERVATION_SPOTS_TOTAL,
103 | 'spots_total': settings.RESERVATION_SPOTS_TOTAL})
104 | if reservation_day[0].spots_free < 1:
105 | return HttpResponseBadRequest(_("No spots free or reservation closed"))
106 |
107 | # Decrement counter on Reservation Day
108 | # SQL: UPDATE field_to_increment = field_to_increment + 1 ...
109 | reservation_day = reservation_day[0]
110 | reservation_day.spots_free = F('spots_free') - 1
111 | reservation_day.save()
112 |
113 | # Create reservation using current model
114 | reservation = form.save(commit=False)
115 | reservation.user = request.user
116 | reservation.date = date
117 | reservation.save()
118 |
119 | # Send email to user that the reservation has been sucessfully placed
120 | send_email(request.user.email, _('New booking | %s' % settings.APP_SHORTNAME), 'email_new.html',
121 | {'name': request.user.username,
122 | 'date': date,
123 | 'reservation_id': reservation.id,
124 | 'extra_data': form,
125 | 'domain': settings.APP_URL})
126 |
127 | reservation_dict = model_to_dict(reservation)
128 | reservation_dict['short_desc'] = reservation.short_desc()
129 | # Send fresh objects to user
130 | return self.render_to_response({"reservation": reservation_dict,
131 | "reservation_day": ReservationDay.objects.get(id=reservation_day.id),
132 | "error": None})
133 |
134 | @method_decorator(login_required)
135 | def delete(self, request):
136 | """Delete user reservation (if canceling reservation is still possible)"""
137 | reservation_id = int(request.REQUEST['id'])
138 | reservation = reservationModel.objects.get(id=reservation_id, user=request.user)
139 | if not reservation:
140 | return HttpResponseBadRequest(_("No such reservation"))
141 | timediff = datetime.datetime.combine(reservation.date, datetime.time()) - datetime.datetime.now()
142 | if timediff.days < 1: # FUTURE TODO: Time resolution setting
143 | return HttpResponseForbidden(_("You have no access to modify this reservation, too late"))
144 | reservation.delete()
145 | # Suuccessfully deleted, increment spots_free for that day
146 | reservation_day = ReservationDay.objects.get(date=reservation.date)
147 | reservation_day.spots_free = F('spots_free') + 1
148 | reservation_day.save()
149 |
150 | return self.render_to_response({"reservation_day": ReservationDay.objects.get(id=reservation_day.id),
151 | "reservation": reservation,
152 | "id": reservation_id,
153 | "error": None})
154 |
155 | @method_decorator(login_required)
156 | def get(self, request):
157 | """Get all user reservations for particular year"""
158 | year = int(request.REQUEST['year'])
159 | reservations = reservationModel.objects.filter(
160 | date__gte=datetime.date(year, 1, 1),
161 | date__lte=datetime.date(year, 12, 31),
162 | user=request.user)
163 | # Convert reservations to dict for easier JSON seralization
164 | reservations_dict = []
165 | for reservation in reservations:
166 | elem = model_to_dict(reservation)
167 | elem['short_desc'] = reservation.short_desc()
168 | reservations_dict.append(elem)
169 |
170 | return self.render_to_response({"reservations": reservations_dict,
171 | "error": None})
172 |
173 |
174 | def get_holidays(request):
175 | """Get holiday days in particular year"""
176 | year = int(request.REQUEST['year'])
177 | holidays = Holiday.objects.filter(
178 | date__gte=datetime.date(year, 1, 1),
179 | date__lte=datetime.date(year, 12, 31),
180 | active=True)
181 | return HttpResponse(json.dumps({"holidays": [model_to_dict(x) for x in holidays], "error": None}, cls=DjangoJSONEncoder), mimetype="application/json")
182 |
183 |
184 | class MonthDetailView(JSONResponseMixin, View):
185 | """Get information about available spots for each day of particular month"""
186 | @method_decorator(login_required)
187 | def get(self, request, month, year):
188 | first, days_month = calendar.monthrange(int(year), int(month))
189 | date_from = datetime.date(int(year), int(month), 1)
190 | date_to = datetime.date(int(year), int(month), days_month)
191 | reservation_days = ReservationDay.objects.filter(
192 | date__gte=date_from,
193 | date__lte=date_to)
194 | return self.render_to_response(reservation_days)
195 |
196 |
197 | @login_required
198 | def calendar_view(request):
199 | """Calendar view available for logged in users"""
200 | from . import get_form, reservationModel
201 | form_details = get_form()
202 | defaults = {
203 | "spots_total": settings.RESERVATION_SPOTS_TOTAL,
204 | "get_extra_data": "true" if reservationModel != SimpleReservation else "false",
205 | "reservations_limit": getattr(settings, 'RESERVATIONS_PER_DAY', 0)
206 | }
207 | return render_to_response("calendar.html", dict(defaults=defaults, form_details=form_details),
208 | context_instance=RequestContext(request))
209 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os, sys
4 | from django.conf import settings
5 |
6 | DIRNAME = os.path.dirname(__file__)
7 | settings.configure(DEBUG=True,
8 | DATABASES={
9 | 'default': {
10 | 'ENGINE': 'django.db.backends.sqlite3',
11 | }
12 | },
13 | ROOT_URLCONF='reservations.urls',
14 | INSTALLED_APPS=('django.contrib.auth',
15 | 'django.contrib.contenttypes',
16 | 'django.contrib.sessions',
17 | 'django.contrib.admin',
18 | 'reservations',),
19 | # App specific settings
20 | RESERVATION_SPOTS_TOTAL=32,
21 | APP_SHORTNAME="test-reservations",
22 | APP_URL="http://127.0.0.1:8000",
23 | EMAIL_FROM="autbot@extensa.pl",),
24 |
25 | from django.test.simple import DjangoTestSuiteRunner
26 | test_runner = DjangoTestSuiteRunner(verbosity=1)
27 | failures = test_runner.run_tests(['reservations', ])
28 | if failures:
29 | sys.exit(failures)
30 |
--------------------------------------------------------------------------------
/screen1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bernii/django-reservations/ff60c6365a1797675edfeb72896f282b746fa2ad/screen1.jpg
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import setup, find_packages
4 | import os
5 |
6 | PACKAGE_DIR = os.path.abspath(os.path.dirname(__file__))
7 | os.chdir(PACKAGE_DIR)
8 |
9 |
10 | setup(name='django-reservations',
11 | version=0.2,
12 | url='https://github.com/bernii/django-reservations',
13 | author="Bernard Kobos",
14 | author_email="bkobos@extensa.pl",
15 | description=("Django module for handling reservations/booking"),
16 | long_description=file(os.path.join(PACKAGE_DIR, 'README.md')).read(),
17 | license='WOW',
18 | packages=find_packages(),
19 | include_package_data=True,
20 | install_requires=[
21 | 'django>=1.4',
22 | ],
23 | # See http://pypi.python.org/pypi?%3Aaction=list_classifiers
24 | classifiers=['Environment :: Web Environment',
25 | 'Framework :: Django',
26 | 'Intended Audience :: Developers',
27 | 'License :: OSI Approved :: WOW License',
28 | 'Operating System :: Unix',
29 | 'Programming Language :: Python']
30 | )
31 |
--------------------------------------------------------------------------------