├── .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 | ![Sample app screenshot using django-reservations](https://raw.github.com/bernii/django-reservations/master/screen1.jpg) 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 | ![Continuous Integration status](https://secure.travis-ci.org/bernii/django-reservations.png) 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 += '
' 249 | if "reservation" in divClass and in_future() 250 | for reservation in dayReservations 251 | _html += '
' 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+='
';}} 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 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
    30 |
  • 31 |
  • 32 |
  • 33 |
34 |
35 |
36 | 37 |
38 | 39 | 52 | 53 | 65 |
{% csrf_token %}
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 | 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 | --------------------------------------------------------------------------------