There are no guests.
23 | {% endif %}
24 | {% endblock %}
25 |
26 |
--------------------------------------------------------------------------------
/payment/migrations/0015_auto_20180303_2208.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-03-03 16:23
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('payment', '0014_auto_20180303_1537'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='checkin',
17 | name='user',
18 | field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
19 | ),
20 | migrations.AlterField(
21 | model_name='checkout',
22 | name='user',
23 | field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/main/templates/main/customer_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 |
8 |
9 |
10 |
15 |
16 |
Room Type: {{ room.get_room_type_display }}
17 |
18 |
19 |
20 |
26 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/payment/migrations/0006_auto_20180219_1818.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-19 12:33
2 |
3 | import django.utils.timezone
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('payment', '0005_checkin_rooms'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='CheckOut',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('check_out_date_time', models.DateTimeField(editable=False, null=True)),
19 | ],
20 | ),
21 | migrations.AddField(
22 | model_name='checkin',
23 | name='check_in_date_time',
24 | field=models.DateTimeField(editable=False, null=True),
25 | ),
26 | migrations.AddField(
27 | model_name='checkin',
28 | name='last_edited_on',
29 | field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
30 | ),
31 | migrations.AddField(
32 | model_name='checkout',
33 | name='check_in',
34 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='payment.CheckIn'),
35 | ),
36 | ]
37 |
--------------------------------------------------------------------------------
/hms/urls.py:
--------------------------------------------------------------------------------
1 | """hms URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/dev/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.conf import settings
17 | from django.conf.urls.static import static
18 | from django.contrib import admin
19 | from django.urls import path, include
20 |
21 | urlpatterns = [
22 | path('admin/', admin.site.urls),
23 | path('', include('main.urls')), # Added url pattern for main app
24 | path('accounts/', include('django.contrib.auth.urls')),
25 | path('payment/', include('payment.urls')),
26 | # path('reserve/', include('reserve.urls')),
27 | ]
28 |
29 | if settings.DEBUG:
30 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
31 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
32 |
--------------------------------------------------------------------------------
/main/templates/payment/checkout_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content-title %}
4 |
{{ title }}
5 | {% endblock %}
6 | {% block main-content %}
7 | {% if checkout_list %}
8 |
9 |
10 |
11 | Check-Out ID
12 | Related Check-in
13 | Stay Duration
14 | Total Amount
15 | Pay Amount
16 | Check-Out Date Time
17 |
18 |
19 |
20 | {% for check_out in checkout_list %}
21 |
22 | {{ check_out }}
23 | {{ check_out.check_in }}
24 | {{ check_out.stay_duration }}
25 | {{ check_out.total_amount }}
26 | {{ check_out.pay_amount }}
27 | {{ check_out.check_out_date_time }}
28 |
29 | {% endfor %}
30 |
31 |
32 | {% else %}
33 |
There are no check-ins.
34 | {% endif %}
35 | {% endblock %}
--------------------------------------------------------------------------------
/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 |
Login
4 | {% endblock %}
5 | {% block login-status %}
6 | {% endblock %}
7 | {% block main-content %}
8 | {% if form.errors %}
9 |
Username and password didn't match. Please Try Again.
10 | {% endif %}
11 | {% if next %}
12 | {% if user.is_authenticated %}
13 |
You don't have access to view this page.
14 | {% else %}
15 |
You must login to view this page.
16 | {% endif %}
17 | {% endif %}
18 |
31 |
32 | {# Assumes you setup the password_reset view in your URLconf #}
33 |
Lost password?
34 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/templates/registration/password_reset_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content-title %}
3 |
Password Change
4 | {% endblock %}
5 | {% block main-content %}
6 |
7 | {% if validlink %}
8 |
Please enter (and confirm) your new password.
9 |
27 | {% else %}
28 |
Password reset failed
29 |
The password reset link was invalid, possibly because it has already been used. Please request a new password
30 | reset.
31 | {% endif %}
32 |
33 | {% endblock %}
--------------------------------------------------------------------------------
/main/templates/main/room_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block main-content %}
4 | {% if room_list %}
5 |
6 |
7 |
8 | Room No
9 | Room Type
10 | Room Price
11 | Availability
12 |
13 |
14 |
15 | {% for room in room_list %}
16 |
17 | {{ room.room_no }}
18 | {{ room.room_type.name }}
19 | {{ room.room_type.price }}
20 | {% if room.availability %}
21 | Available {% else %}
22 | Not Available {% endif %}
23 |
24 |
25 | {% endfor %}
26 |
27 |
28 |
Filter By:
29 | {% if filter == 'all' %}Available{% else %}All{% endif %}
30 | {% else %}
31 |
There are no rooms.
32 | {% endif %}
33 | {% endblock %}
34 |
35 |
--------------------------------------------------------------------------------
/main/templates/customer_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 |
{{ title }}
4 | {% endblock %}
5 |
6 | {% block main-content %}
7 |
32 | {% endblock %}
--------------------------------------------------------------------------------
/main/templates/payment_index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 |
{{ title }}
4 | {% endblock %}
5 |
6 | {% block main-content %}
7 |
8 |
9 |
10 |
11 |
{{ num_of_reservations }}
12 |
13 |
14 |
15 |
16 |
17 |
{{ num_of_check_ins }}
18 |
19 |
20 |
21 |
22 |
23 | {% if last_checked_in.id %}
24 |
26 | {% else %}
27 |
Not Yet
28 | {% endif %}
29 |
30 |
31 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/main/templates/payment/checkin_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 |
Check-In No: {{ checkin.id }}
4 | {% endblock %}
5 |
6 | {% block main-content %}
7 |
8 |
9 |
12 |
Rooms:
13 | {% for room in rooms %}
14 |
{{ room }} {% if not forloop.last %}, {% endif %}
15 | {% empty %}No rooms in the reservation.
16 | {% endfor %}
17 |
18 |
19 |
20 |
21 |
23 |
28 |
29 |
30 |
31 | {% endblock %}
32 |
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Text backup files
2 | *.bak
3 |
4 | #Database
5 | *.sqlite3
6 |
7 | #IDE
8 | .idea/
9 | .git/
10 | # Byte-compiled / optimized / DLL files
11 | __pycache__/
12 | *.py[cod]
13 | *$py.class
14 |
15 | # C extensions
16 | *.so
17 |
18 | # Distribution / packaging
19 | .Python
20 | env/
21 | build/
22 | develop-eggs/
23 | dist/
24 | downloads/
25 | eggs/
26 | .eggs/
27 | lib/
28 | lib64/
29 | parts/
30 | sdist/
31 | var/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 |
36 | # PyInstaller
37 | # Usually these files are written by a python script from a template
38 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
39 | *.manifest
40 | *.spec
41 |
42 | # Installer logs
43 | pip-log.txt
44 | pip-delete-this-directory.txt
45 |
46 | # Unit test / coverage reports
47 | htmlcov/
48 | .tox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *,cover
55 | .hypothesis/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | target/
77 |
78 | # IPython Notebook
79 | .ipynb_checkpoints
80 |
81 | # pyenv
82 | .python-version
83 |
84 | # celery beat schedule file
85 | celerybeat-schedule
86 |
87 | # dotenv
88 | .env
89 |
90 | # virtualenv
91 | venv/
92 | ENV/
93 |
94 | # Spyder project settings
95 | .spyderproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
--------------------------------------------------------------------------------
/main/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import login_required
2 | from django.urls import path
3 |
4 | from . import views
5 |
6 | urlpatterns = [
7 | # The field option name, should be unique as it is a unique identifier.
8 | # e.g. if you want to use the hyperlink of index page, don't use hardcoded url like
9 | # href="/main/" as if it is changed we have to change all the occurrences of it.
10 | # to solve this, use the value from name field. for above example use
11 | # href="{% url 'index' %}
12 | path('', views.index, name='index'),
13 | path('signup/', views.signup, name='signup'),
14 | path('rooms/', views.RoomListView.as_view(), name='rooms'), # List of Rooms
15 | path('reservations/', views.ReservationListView.as_view(), name='reservations'), # List of Reservations
16 | #
takes the argument sent in urls.
17 | path('room/', views.RoomDetailView.as_view(), name='room-detail'), # Detail of each room
18 | # Detail of each reservation
19 | path('reservation/', views.ReservationDetailView.as_view(), name='reservation-detail'),
20 | path('customer/', views.CustomerDetailView.as_view(), name='customer-detail'), # Detail of each customer
21 | path('staff/', views.StaffDetailView.as_view(), name='staff-detail'), # Detail of staff
22 | path('profile/', login_required(views.ProfileView.as_view()), name='profile'),
23 | path('guests/', views.GuestListView.as_view(), name='guest-list'),
24 | path('reserve/', views.reserve, name='reserve'), # For reservation
25 | ]
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hotel-Management-System
2 | **Hotel Management System** is under development.
3 | * Clone using SSH key for your own good.
4 | * You can refer to this [link](https://help.github.com/articles/connecting-to-github-with-ssh/) connecting to Github with SSH keys so that you don't need username and password everytime you perform push or pull request.
5 | * Otherwise use HTTPS but you will need password everytime.
6 | ### The software project contributors are required to install the dependencies as follows:
7 | 1. If you want to install system wide:
8 | ```shell
9 | sudo pip install -r requirements.txt
10 | ```
11 | 2. If you want to use `virtualenv`:
12 | * Create a virtual environment as (you can use any name of the environment as you like):
13 | ```shell
14 | virtualenv -p python3 hms-virtual-env
15 | ```
16 | * Then execute this:
17 | ```shell
18 | source hms-virtual-env/bin/activate
19 | ```
20 | * The install requirements as:
21 | ```shell
22 | pip install -r requirements.txt
23 | ```
24 | __Note:__ To deactivate `virtualenv`, execute
25 | ```shell
26 | deactivate
27 | ```
28 | ### Finally:
29 | * Go to Hotel-Management-System folder that you cloned before.
30 | * Execute the followings:
31 | ```shell
32 | python3 manage.py migrate
33 | ```
34 | Then,
35 | ```shell
36 | python3 manage.py runserver
37 | ```
38 | * This will work if correctly set up.
39 | ## For more information, visit [Django Documentation](https://docs.djangoproject.com/en/2.0/).
40 |
41 | *You are supposed to be using linux environment with **Python 3**. Everything else is simple. The packages mentioned in requirements files are required for deployment especially in heroku.*
42 |
--------------------------------------------------------------------------------
/main/templates/main/reservation_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 | Reservation No. {{ reservation.reservation_id }} Details
4 | {% endblock %}
5 | {% block main-content %}
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
28 | {% endblock %}
--------------------------------------------------------------------------------
/main/static/css/styles.css:
--------------------------------------------------------------------------------
1 | /* Move down content because we have a fixed navbar that is 3.5rem tall */
2 | body {
3 | padding-top: 3.5rem;
4 | }
5 |
6 | /*
7 | * Typography
8 | */
9 |
10 | h1 {
11 | padding-bottom: 9px;
12 | margin-bottom: 20px;
13 | border-bottom: 1px solid #eee;
14 | }
15 |
16 | /*
17 | * Sidebar
18 | */
19 |
20 | .sidebar {
21 | position: fixed;
22 | top: 51px;
23 | bottom: 0;
24 | left: 0;
25 | z-index: 1000;
26 | padding: 20px 0;
27 | overflow-x: hidden;
28 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
29 | border-right: 1px solid #eee;
30 | }
31 |
32 | .sidebar .nav {
33 | margin-bottom: 20px;
34 | }
35 |
36 | .sidebar .nav-item {
37 | width: 100%;
38 | }
39 |
40 | .sidebar .nav-item + .nav-item {
41 | margin-left: 0;
42 | }
43 |
44 | .sidebar .nav-link {
45 | border-radius: 0;
46 | }
47 |
48 | .fa-check-circle {
49 | color: #428bca;
50 | }
51 |
52 | .fa-times-circle {
53 | color: #d9534f;
54 | }
55 |
56 | /*
57 | * Dashboard
58 | */
59 |
60 | /* Placeholders */
61 | .placeholders {
62 | padding-bottom: 3rem;
63 | }
64 |
65 | .placeholder img {
66 | padding-top: 1.5rem;
67 | padding-bottom: 1.5rem;
68 | }
69 |
70 | /* Forms */
71 | .fieldset {
72 | border: 5px groove darkgrey !important;
73 | padding: 0 1.4em 1.4em 1.4em !important;
74 | margin: 0 0 1.5em 0 !important;
75 | }
76 |
77 | .legend {
78 | width: auto;
79 | border: none;
80 | padding: 5px 5px 5px 5px;
81 | }
82 |
83 | .notMatched {
84 | font-weight: bold;
85 | font-size: 2rem;
86 | color: #a41515;
87 | }
88 |
89 | select {
90 | margin: 2px 0;
91 | padding: 2px 3px;
92 | vertical-align: middle;
93 | font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif;
94 | font-weight: normal;
95 | font-size: 13px;
96 | border: 1px solid #ccc;
97 | border-radius: 4px;
98 | padding: 5px 6px;
99 | margin-top: 0;
100 | }
101 |
102 | select:focus {
103 | border-color: #999;
104 | }
105 |
106 | select {
107 | height: 30px;
108 | }
109 |
110 | select[multiple] {
111 | min-height: 150px;
112 | }
113 |
114 | .errors select {
115 | border: 1px solid #ba2121;
116 | }
117 |
118 |
119 |
--------------------------------------------------------------------------------
/main/templates/signup.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 | Signup Form
4 | {% endblock %}
5 | {% block sidebar %}
6 | {% endblock %}
7 | {% block main-content %}
8 |
61 | {% endblock %}
--------------------------------------------------------------------------------
/main/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content-title %}
3 | {{ title }}
4 | {% endblock %}
5 |
6 | {% block main-content %}
7 |
8 |
9 |
10 |
11 |
{{ total_num_rooms }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
{{ available_num_rooms }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
{{ total_num_reservations }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
{{ total_num_staffs }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
{{ total_num_customers }}
40 |
41 |
42 | {% if user.is_authenticated %}
43 |
44 |
45 |
46 | {% if last_reserved_by.reservation_id %}
47 |
50 |
51 | Reserved By:
52 | {% if perms.main.can_view_staff_detail %}
53 |
{{ last_reserved_by.staff }}
54 | {% else %}{{ last_reserved_by.staff }}
55 | {% endif %}
56 |
57 | {% else %}
58 |
Not Yet
59 | {% endif %}
60 |
61 |
62 | {% endif %}
63 |
64 | {% endblock %}
65 |
--------------------------------------------------------------------------------
/main/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import *
4 |
5 |
6 | # Register your models here.
7 |
8 | @admin.register(Staff)
9 | class StaffAdmin(admin.ModelAdmin):
10 | # To show in admin app
11 | list_display = (
12 | 'staff_id',
13 | 'user',
14 | 'first_name',
15 | 'middle_name',
16 | 'last_name',
17 | 'contact_no',
18 | 'address',
19 | 'email_address',
20 | )
21 | # Adding search bar
22 | search_fields = [
23 | 'staff_id',
24 | 'user',
25 | 'first_name',
26 | 'middle_name',
27 | 'last_name',
28 | 'contact_no',
29 | 'address',
30 | 'email_address',
31 | ]
32 | # Categorizing the fields
33 | fieldsets = (
34 | (None, {
35 | 'fields': ('profile_picture', ('first_name', 'middle_name', 'last_name'),)
36 | }),
37 | ('Contact Information', {
38 | 'fields': (('contact_no', 'email_address'), 'address')
39 | })
40 | )
41 |
42 |
43 | @admin.register(Customer)
44 | class CustomerAdmin(admin.ModelAdmin):
45 | list_display = (
46 | 'customer_id',
47 | 'first_name',
48 | 'middle_name',
49 | 'last_name',
50 | 'contact_no',
51 | 'address',
52 | 'email_address',
53 | )
54 | search_fields = [
55 | 'customer_id',
56 | 'first_name',
57 | 'middle_name',
58 | 'last_name',
59 | 'contact_no',
60 | 'address',
61 | 'email_address',
62 | ]
63 |
64 | fieldsets = (
65 | (None, {
66 | 'fields': (('first_name', 'middle_name', 'last_name'),)
67 | }),
68 | ('Contact Information', {
69 | 'fields': (('contact_no', 'email_address'), 'address')
70 | })
71 | )
72 |
73 |
74 | @admin.register(Reservation)
75 | class ReservationAdmin(admin.ModelAdmin):
76 | list_display = (
77 | 'reservation_id',
78 | 'customer',
79 | 'staff',
80 | 'no_of_children',
81 | 'no_of_adults',
82 | 'reservation_date_time',
83 | 'expected_arrival_date_time',
84 | 'expected_departure_date_time',
85 | )
86 |
87 |
88 | @admin.register(Room)
89 | class RoomAdmin(admin.ModelAdmin):
90 | list_display = (
91 | 'room_no',
92 | 'room_type',
93 | 'reservation',
94 | 'availability',
95 | 'display_facility',
96 | )
97 | # Adding filter
98 | list_filter = ('room_type', 'availability')
99 | filter_horizontal = ('facility',)
100 | fields = (('room_no', 'room_type'), 'reservation', 'facility')
101 | search_fields = [
102 | 'reservation__customer__first_name',
103 | 'reservation__customer__middle_name',
104 | 'reservation__customer__last_name',
105 | ]
106 |
107 |
108 | @admin.register(Facility)
109 | class FacilityAdmin(admin.ModelAdmin):
110 | list_display = ('name', 'price')
111 |
112 |
113 | @admin.register(RoomType)
114 | class RoomTypeAdmin(admin.ModelAdmin):
115 | list_display = ('name', 'price')
116 |
--------------------------------------------------------------------------------
/main/static/img/selector-icons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------
/payment/models.py:
--------------------------------------------------------------------------------
1 | from math import ceil
2 |
3 | from django.contrib.auth.models import User
4 | from django.db import models
5 | from django.shortcuts import get_object_or_404
6 | from django.urls import reverse
7 | from django.utils import timezone
8 | from django.utils.encoding import python_2_unicode_compatible
9 | from django.http import HttpRequest
10 |
11 | from main.models import Reservation, Room
12 |
13 |
14 | # Create your models here.
15 | @python_2_unicode_compatible
16 | class CheckIn(models.Model):
17 | id = models.CharField(max_length=50, primary_key=True, blank=True, editable=False)
18 | reservation = models.OneToOneField(Reservation, on_delete=models.CASCADE)
19 | rooms = models.CharField(max_length=50, editable=False, blank=True)
20 | initial_amount = models.PositiveSmallIntegerField(blank=True, editable=False)
21 | check_in_date_time = models.DateTimeField(default=timezone.now, editable=False)
22 | last_edited_on = models.DateTimeField(default=timezone.now, editable=False)
23 | user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, editable=False)
24 |
25 | def __str__(self):
26 | return "%i - %s" % (self.reservation.reservation_id, self.reservation.customer)
27 |
28 | def get_absolute_url(self):
29 | return reverse('check_in-detail', args=[self.id])
30 |
31 | def save(self, *args, **kwargs):
32 | if not self.id:
33 | self.check_in_date_time = timezone.now()
34 | self.id = "checkin_" + str(self.reservation.reservation_id)
35 | self.rooms = ', '.join(room.room_no for room in self.reservation.room_set.all())
36 | self.initial_amount = 0
37 | for room in self.reservation.room_set.all():
38 | self.initial_amount += room.room_type.price
39 | for facility in room.facility.all():
40 | self.initial_amount += facility.price
41 | else:
42 | reservation = get_object_or_404(CheckIn, id=self.id).reservation
43 | if self.reservation != reservation:
44 | self.reservation = reservation
45 |
46 | self.last_edited_on = timezone.now()
47 | super().save(*args, **kwargs)
48 |
49 |
50 | @python_2_unicode_compatible
51 | class CheckOut(models.Model):
52 | check_in = models.OneToOneField(CheckIn, on_delete=models.CASCADE)
53 | stay_duration = models.DurationField(null=True, editable=False)
54 | total_amount = models.PositiveSmallIntegerField(default=0, editable=False)
55 | pay_amount = models.PositiveSmallIntegerField(default=0, editable=False)
56 | check_out_date_time = models.DateTimeField(editable=False, null=True)
57 | user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, editable=False)
58 |
59 | def __str__(self):
60 | return str(self.id)
61 |
62 | def save(self, *args, **kwargs):
63 | if not self.id:
64 | self.check_out_date_time = timezone.now()
65 | self.stay_duration = self.check_out_date_time - self.check_in.check_in_date_time
66 | calculated_duration = timezone.timedelta(days=ceil(self.stay_duration.total_seconds() / 3600 / 24))
67 | self.total_amount = calculated_duration.days * self.check_in.initial_amount
68 | self.pay_amount = self.total_amount - self.check_in.initial_amount
69 | super().save(*args, **kwargs)
70 |
--------------------------------------------------------------------------------
/main/widgets.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | # from django.conf import settings
3 | from django.utils.translation import gettext as _
4 |
5 |
6 | class MyDateWidget(forms.DateInput):
7 | @property
8 | def media(self):
9 | # extra = '' if settings.DEBUG else '.min'
10 | js = [
11 | # 'vendor/jquery/jquery%s.js' % extra,
12 | 'jquery.js',
13 | 'jquery.init.js',
14 | 'calendar.js',
15 | 'DateTimeShortcuts.js',
16 | ]
17 | return forms.Media(js=["js/%s" % path for path in js])
18 |
19 | def __init__(self, attrs=None, format=None):
20 | final_attrs = {'class': 'vDateField', 'size': '10'}
21 | if attrs is not None:
22 | final_attrs.update(attrs)
23 | super().__init__(attrs=final_attrs, format=format)
24 |
25 |
26 | class MyTimeWidget(forms.TimeInput):
27 | @property
28 | def media(self):
29 | # extra = '' if settings.DEBUG else '.min'
30 | js = [
31 | # 'vendor/jquery/jquery%s.js' % extra,
32 | 'jquery.js',
33 | 'jquery.init.js',
34 | 'calendar.js',
35 | 'DateTimeShortcuts.js',
36 | ]
37 | return forms.Media(js=["js/%s" % path for path in js])
38 |
39 | def __init__(self, attrs=None, format=None):
40 | final_attrs = {'class': 'vTimeField', 'size': '8'}
41 | if attrs is not None:
42 | final_attrs.update(attrs)
43 | super().__init__(attrs=final_attrs, format=format)
44 |
45 |
46 | class MySplitDateTime(forms.SplitDateTimeWidget):
47 | """
48 | A SplitDateTime Widget.
49 | """
50 | template_name = 'split_datetime.html'
51 |
52 | def __init__(self, attrs=None):
53 | widgets = [MyDateWidget, MyTimeWidget]
54 | # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
55 | # we want to define widgets.
56 | forms.MultiWidget.__init__(self, widgets, attrs)
57 |
58 | def get_context(self, name, value, attrs):
59 | context = super().get_context(name, value, attrs)
60 | context['date_label'] = _('Date:')
61 | context['time_label'] = _('Time:')
62 | return context
63 |
64 |
65 | class FilteredSelectMultiple(forms.SelectMultiple):
66 | """
67 | A SelectMultiple with a JavaScript filter interface.
68 |
69 | Note that the resulting JavaScript assumes that the jsi18n
70 | catalog has been loaded in the page
71 | """
72 |
73 | @property
74 | def media(self):
75 | js = [
76 | 'jquery.js',
77 | 'jquery.init.js',
78 | 'core.js',
79 | 'SelectBox.js',
80 | 'SelectFilter2.js',
81 | ]
82 | return forms.Media(js=["js/%s" % path for path in js])
83 |
84 | def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
85 | self.verbose_name = verbose_name
86 | self.is_stacked = is_stacked
87 | super().__init__(attrs, choices)
88 |
89 | def get_context(self, name, value, attrs):
90 | context = super().get_context(name, value, attrs)
91 | context['widget']['attrs']['class'] = 'selectfilter'
92 | if self.is_stacked:
93 | context['widget']['attrs']['class'] += 'stacked'
94 | context['widget']['attrs']['data-field-name'] = self.verbose_name
95 | context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
96 | return context
97 |
--------------------------------------------------------------------------------
/main/templates/main/reservation_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block main-content %}
4 |
7 | {% if reservation_list %}
8 |
9 |
10 |
11 | Reservation ID
12 | Customer
13 | Staff
14 | No of Children
15 | No of Adults
16 | Rooms
17 | Reservation Date Time
18 | Check In?
19 |
20 |
21 |
22 | {% for reservation in reservation_list %}
23 |
24 |
25 | {{ reservation.reservation_id }}
26 |
27 | {{ reservation.customer }}
28 | {{ reservation.staff }}
29 | {{ reservation.no_of_children }}
30 | {{ reservation.no_of_adults }}
31 |
32 | {% for room in reservation.room_set.all %}
33 | {{ room.room_no }}{% if not forloop.last %},{% endif %}
34 | {% endfor %}
35 |
36 | {{ reservation.reservation_date_time }}
37 | {% if not reservation.checkin %}
38 |
41 | Check-In
42 |
43 |
44 | {% else %}
45 | Details
47 | {% endif %}
48 |
49 | {% endfor %}
50 |
51 |
52 |
53 |
54 |
55 |
59 |
60 |
Do you want to confirm check-in?
61 |
62 |
68 |
69 |
70 |
71 | {% else %}
72 | There are no reservations.
73 | {% endif %}
74 | {% endblock %}
75 | {% block additional-javascript %}
76 |
84 | {% endblock %}
--------------------------------------------------------------------------------
/main/templates/payment/checkin_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content-title %}
4 | {{ title }}
5 | {% endblock %}
6 | {% block main-content %}
7 | {% if checkin_list %}
8 |
11 |
12 |
13 |
14 | Check-In
15 | Reservation
16 | Rooms
17 | Paid Amount
18 | Check-In Date Time
19 | Checkout?
20 |
21 |
22 |
23 | {% for check_in in checkin_list %}
24 |
25 |
26 | {{ check_in }}
27 |
28 | {{ check_in.reservation }}
29 | {% if check_in.rooms %}{{ check_in.rooms }}{% else %}-{% endif %}
30 | {{ check_in.initial_amount }}
31 | {{ check_in.check_in_date_time }}
32 |
33 | {% if not check_in.checkout %}
34 |
37 | Checkout
38 |
39 |
40 | {% else %}
41 | Details
43 | {% endif %}
44 |
45 |
46 | {% endfor %}
47 |
48 |
49 |
50 |
51 |
52 |
56 |
57 |
Do you want to confirm checkout?
58 |
59 |
65 |
66 |
67 |
68 | {% else %}
69 | There are no check-ins.
70 | {% endif %}
71 | {% block additional-javascript %}
72 |
87 | {% endblock %}
88 | {% endblock %}
89 |
--------------------------------------------------------------------------------
/main/static/js/jsi18n.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | (function(globals) {
4 |
5 | var django = globals.django || (globals.django = {});
6 |
7 |
8 | django.pluralidx = function(count) { return (count == 1) ? 0 : 1; };
9 |
10 |
11 | /* gettext library */
12 |
13 | django.catalog = django.catalog || {};
14 |
15 |
16 | if (!django.jsi18n_initialized) {
17 | django.gettext = function(msgid) {
18 | var value = django.catalog[msgid];
19 | if (typeof(value) == 'undefined') {
20 | return msgid;
21 | } else {
22 | return (typeof(value) == 'string') ? value : value[0];
23 | }
24 | };
25 |
26 | django.ngettext = function(singular, plural, count) {
27 | var value = django.catalog[singular];
28 | if (typeof(value) == 'undefined') {
29 | return (count == 1) ? singular : plural;
30 | } else {
31 | return value[django.pluralidx(count)];
32 | }
33 | };
34 |
35 | django.gettext_noop = function(msgid) { return msgid; };
36 |
37 | django.pgettext = function(context, msgid) {
38 | var value = django.gettext(context + '\x04' + msgid);
39 | if (value.indexOf('\x04') != -1) {
40 | value = msgid;
41 | }
42 | return value;
43 | };
44 |
45 | django.npgettext = function(context, singular, plural, count) {
46 | var value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count);
47 | if (value.indexOf('\x04') != -1) {
48 | value = django.ngettext(singular, plural, count);
49 | }
50 | return value;
51 | };
52 |
53 | django.interpolate = function(fmt, obj, named) {
54 | if (named) {
55 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
56 | } else {
57 | return fmt.replace(/%s/g, function(match){return String(obj.shift())});
58 | }
59 | };
60 |
61 |
62 | /* formatting library */
63 |
64 | django.formats = {
65 | "DATETIME_FORMAT": "N j, Y, P",
66 | "DATETIME_INPUT_FORMATS": [
67 | "%Y-%m-%d %H:%M:%S",
68 | "%Y-%m-%d %H:%M:%S.%f",
69 | "%Y-%m-%d %H:%M",
70 | "%Y-%m-%d",
71 | "%m/%d/%Y %H:%M:%S",
72 | "%m/%d/%Y %H:%M:%S.%f",
73 | "%m/%d/%Y %H:%M",
74 | "%m/%d/%Y",
75 | "%m/%d/%y %H:%M:%S",
76 | "%m/%d/%y %H:%M:%S.%f",
77 | "%m/%d/%y %H:%M",
78 | "%m/%d/%y"
79 | ],
80 | "DATE_FORMAT": "N j, Y",
81 | "DATE_INPUT_FORMATS": [
82 | "%Y-%m-%d",
83 | "%m/%d/%Y",
84 | "%m/%d/%y"
85 | ],
86 | "DECIMAL_SEPARATOR": ".",
87 | "FIRST_DAY_OF_WEEK": 0,
88 | "MONTH_DAY_FORMAT": "F j",
89 | "NUMBER_GROUPING": 3,
90 | "SHORT_DATETIME_FORMAT": "m/d/Y P",
91 | "SHORT_DATE_FORMAT": "m/d/Y",
92 | "THOUSAND_SEPARATOR": ",",
93 | "TIME_FORMAT": "P",
94 | "TIME_INPUT_FORMATS": [
95 | "%H:%M:%S",
96 | "%H:%M:%S.%f",
97 | "%H:%M"
98 | ],
99 | "YEAR_MONTH_FORMAT": "F Y"
100 | };
101 |
102 | django.get_format = function(format_type) {
103 | var value = django.formats[format_type];
104 | if (typeof(value) == 'undefined') {
105 | return format_type;
106 | } else {
107 | return value;
108 | }
109 | };
110 |
111 | /* add to global namespace */
112 | globals.pluralidx = django.pluralidx;
113 | globals.gettext = django.gettext;
114 | globals.ngettext = django.ngettext;
115 | globals.gettext_noop = django.gettext_noop;
116 | globals.pgettext = django.pgettext;
117 | globals.npgettext = django.npgettext;
118 | globals.interpolate = django.interpolate;
119 | globals.get_format = django.get_format;
120 |
121 | django.jsi18n_initialized = true;
122 | }
123 |
124 | }(this));
125 |
126 |
--------------------------------------------------------------------------------
/hms/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for hms project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.1.dev20171216185936.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/dev/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/dev/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 | # Quick-start development settings - unsuitable for production
19 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
20 |
21 | # SECURITY WARNING: keep the secret key used in production secret!
22 | SECRET_KEY = '(s5rdh$7wemmtef#pror9m%3zcbb*9apisqh9_us*gu^5chm_a'
23 |
24 | # SECURITY WARNING: don't run with debug turned on in production!
25 | DEBUG = True
26 |
27 | ALLOWED_HOSTS = []
28 |
29 | # Application definition
30 |
31 | INSTALLED_APPS = [
32 | 'django.contrib.admin',
33 | 'django.contrib.auth',
34 | 'django.contrib.contenttypes',
35 | 'django.contrib.sessions',
36 | 'django.contrib.messages',
37 | 'django.contrib.staticfiles',
38 | 'main.apps.MainConfig',
39 | 'payment.apps.PaymentConfig',
40 | ]
41 |
42 | MIDDLEWARE = [
43 | 'django.middleware.security.SecurityMiddleware',
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.common.CommonMiddleware',
46 | 'django.middleware.csrf.CsrfViewMiddleware',
47 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
48 | 'django.contrib.messages.middleware.MessageMiddleware',
49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
50 | ]
51 |
52 | ROOT_URLCONF = 'hms.urls'
53 |
54 | TEMPLATES = [
55 | {
56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
57 | 'DIRS': ['./templates', ],
58 | 'APP_DIRS': True,
59 | 'OPTIONS': {
60 | 'context_processors': [
61 | 'django.template.context_processors.debug',
62 | 'django.template.context_processors.request',
63 | 'django.contrib.auth.context_processors.auth',
64 | 'django.contrib.messages.context_processors.messages',
65 | 'django.template.context_processors.media',
66 | ],
67 | },
68 | },
69 | ]
70 |
71 | WSGI_APPLICATION = 'hms.wsgi.application'
72 |
73 | # Database
74 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases
75 |
76 | DATABASES = {
77 | 'default': {
78 | 'ENGINE': 'django.db.backends.sqlite3',
79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
80 | 'ATOMIC_REQUESTS': True,
81 | }
82 | }
83 |
84 | # Password validation
85 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
86 |
87 | AUTH_PASSWORD_VALIDATORS = [
88 | {
89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
90 | },
91 | {
92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
93 | },
94 | {
95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
96 | },
97 | {
98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
99 | },
100 | ]
101 |
102 | # Internationalization
103 | # https://docs.djangoproject.com/en/dev/topics/i18n/
104 |
105 | LANGUAGE_CODE = 'en-us'
106 |
107 | TIME_ZONE = 'Asia/Kathmandu'
108 |
109 | USE_I18N = True
110 |
111 | USE_L10N = True
112 |
113 | USE_TZ = True
114 |
115 | # Redirect to home URL after login (Default redirects to /accounts/profile/)
116 | LOGIN_REDIRECT_URL = '/'
117 | LOGOUT_REDIRECT_URL = '/'
118 | # For password reset
119 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
120 | # For session expire
121 | SESSION_EXPIRE_AT_BROWSER_CLOSE = True
122 | # Static files (CSS, JavaScript, Images)
123 | # https://docs.djangoproject.com/en/dev/howto/static-files/
124 | STATIC_URL = '/static/'
125 | STATIC_ROOT = os.path.join(BASE_DIR, 'static')
126 |
127 | MEDIA_URL = '/media/'
128 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
129 |
--------------------------------------------------------------------------------
/payment/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import permission_required
2 | from django.contrib.auth.mixins import PermissionRequiredMixin
3 | from django.db import transaction, IntegrityError
4 | from django.http import Http404
5 | from django.shortcuts import render
6 | from django.urls import reverse_lazy
7 | from django.views import generic
8 |
9 | from main.models import Facility, Reservation, Staff
10 | from .forms import CheckoutRequestForm
11 | from .models import CheckIn, CheckOut
12 |
13 |
14 | # Create your views here.
15 | @permission_required('main.can_view_staff', login_url='login')
16 | def payment_index(request):
17 | title = "Payment Dashboard"
18 | num_of_reservations = Reservation.objects.all().count()
19 | num_of_check_ins = CheckIn.objects.all().count()
20 | last_checked_in = CheckIn.objects.none()
21 | if num_of_check_ins > 0:
22 | last_checked_in = CheckIn.objects.get_queryset().latest('check_in_date_time')
23 | return render(
24 | request,
25 | 'payment_index.html', {
26 | 'title': title,
27 | 'num_of_reservations': num_of_reservations,
28 | 'num_of_check_ins': num_of_check_ins,
29 | 'last_checked_in': last_checked_in,
30 | }
31 | )
32 |
33 |
34 | class CheckInListView(PermissionRequiredMixin, generic.ListView, generic.FormView):
35 | model = CheckIn
36 | paginate_by = 5
37 | queryset = CheckIn.objects.all().order_by('-check_in_date_time')
38 | allow_empty = True
39 | permission_required = 'main.can_view_customer'
40 | title = "Check-In List"
41 | form_class = CheckoutRequestForm
42 | extra_context = {
43 | 'title': title,
44 | }
45 | success_url = reverse_lazy('check_out-list')
46 |
47 | @transaction.atomic
48 | def form_valid(self, form):
49 | try:
50 | with transaction.atomic():
51 | checkout = form.save(commit=False)
52 | checkout.user = self.request.user
53 | checkout.save()
54 | for room in checkout.check_in.reservation.room_set.all():
55 | room.reservation = None
56 | room.save()
57 | except IntegrityError:
58 | raise Http404
59 | return super().form_valid(form)
60 |
61 |
62 | class CheckInDetailView(PermissionRequiredMixin, generic.DetailView):
63 | model = CheckIn
64 | permission_required = 'main.can_view_customer'
65 | num_facilities = Facility.objects.count()
66 | title = "Check-In Detail"
67 | if not num_facilities:
68 | facilities = Facility.objects.none()
69 | else:
70 | facilities = Facility.objects.all()
71 |
72 | extra_context = {
73 | 'facilities': facilities,
74 | 'num_facilities': num_facilities,
75 | 'title': title,
76 | }
77 |
78 | def get_context_data(self, **kwargs):
79 | context = super().get_context_data(**kwargs)
80 | checkin = context['checkin']
81 | rooms = checkin.rooms
82 | staff = Staff.objects.filter(user=checkin.user)
83 | if not staff.count():
84 | staff = Staff.objects.none()
85 | else:
86 | staff = Staff.objects.get(user=checkin.user)
87 | context['staff'] = staff
88 | if rooms:
89 | new_rooms = checkin.rooms.split(', ')
90 | new_rooms = list(map(int, new_rooms))
91 | context['rooms'] = new_rooms
92 | return context
93 |
94 |
95 | class CheckOutListView(PermissionRequiredMixin, generic.ListView):
96 | model = CheckOut
97 | paginate_by = 5
98 | allow_empty = True
99 | queryset = CheckOut.objects.all().order_by('-check_out_date_time')
100 | permission_required = 'main.can_view_customer'
101 | title = "Check-Out List"
102 | extra_context = {
103 | 'title': title,
104 | }
105 |
106 |
107 | class CheckOutDetailView(PermissionRequiredMixin, generic.DetailView):
108 | model = CheckOut
109 | permission_required = 'main.can_view_customer'
110 | title = "Check-Out Detail"
111 | extra_context = {
112 | 'title': title,
113 | }
114 |
115 | def get_context_data(self, **kwargs):
116 | context = super().get_context_data(**kwargs)
117 | checkout = context['checkout']
118 |
119 | staff = Staff.objects.filter(user=checkout.user)
120 | if not staff.count():
121 | staff = Staff.objects.none()
122 | else:
123 | staff = Staff.objects.get(user=checkout.user)
124 | context['staff'] = staff
125 |
126 | return context
127 |
128 |
--------------------------------------------------------------------------------
/main/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-09 11:00
2 |
3 | import django.utils.timezone
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Customer',
19 | fields=[
20 | ('customer_id', models.AutoField(primary_key=True, serialize=False)),
21 | ('first_name', models.CharField(max_length=50)),
22 | ('middle_name', models.CharField(blank=True, max_length=50)),
23 | ('last_name', models.CharField(max_length=50)),
24 | ('contact_no', models.CharField(max_length=15)),
25 | ('address', models.CharField(max_length=100)),
26 | ('email_address', models.EmailField(blank=True, max_length=254, null=True)),
27 | ],
28 | options={
29 | 'ordering': ['first_name', 'middle_name', 'last_name'],
30 | 'permissions': (('can_view_customer', 'Can view customer'),),
31 | },
32 | ),
33 | migrations.CreateModel(
34 | name='Facility',
35 | fields=[
36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
37 | ('name', models.CharField(max_length=10)),
38 | ('price', models.PositiveSmallIntegerField()),
39 | ],
40 | options={
41 | 'verbose_name_plural': 'Facilities',
42 | },
43 | ),
44 | migrations.CreateModel(
45 | name='Reservation',
46 | fields=[
47 | ('reservation_id', models.AutoField(primary_key=True, serialize=False)),
48 | ('no_of_children', models.PositiveSmallIntegerField(default=0)),
49 | ('no_of_adults', models.PositiveSmallIntegerField(default=1)),
50 | ('reservation_date_time', models.DateTimeField(default=django.utils.timezone.now)),
51 | ('expected_arrival_date_time', models.DateTimeField(default=django.utils.timezone.now)),
52 | ('expected_departure_date_time', models.DateTimeField(default=django.utils.timezone.now)),
53 | ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Customer')),
54 | ],
55 | options={
56 | 'permissions': (('can_view_reservation', 'Can view reservation'), ('can_view_reservation_detail', 'Can view reservation detail')),
57 | },
58 | ),
59 | migrations.CreateModel(
60 | name='Room',
61 | fields=[
62 | ('room_no', models.CharField(max_length=10, primary_key=True, serialize=False)),
63 | ('availability', models.BooleanField(default=0)),
64 | ('facility', models.ManyToManyField(to='main.Facility')),
65 | ('reservation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='main.Reservation')),
66 | ],
67 | options={
68 | 'ordering': ['room_no'],
69 | 'permissions': (('can_view_room', 'Can view room'),),
70 | },
71 | ),
72 | migrations.CreateModel(
73 | name='RoomType',
74 | fields=[
75 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
76 | ('name', models.CharField(max_length=10)),
77 | ('price', models.PositiveSmallIntegerField()),
78 | ],
79 | ),
80 | migrations.CreateModel(
81 | name='Staff',
82 | fields=[
83 | ('staff_id', models.AutoField(primary_key=True, serialize=False)),
84 | ('first_name', models.CharField(max_length=50)),
85 | ('middle_name', models.CharField(blank=True, max_length=50)),
86 | ('last_name', models.CharField(max_length=50)),
87 | ('contact_no', models.CharField(max_length=15)),
88 | ('address', models.CharField(max_length=100)),
89 | ('email_address', models.EmailField(max_length=254)),
90 | ('user', models.OneToOneField(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
91 | ],
92 | options={
93 | 'ordering': ['first_name', 'middle_name', 'last_name'],
94 | 'permissions': (('can_view_staff', 'Can view staff'), ('can_view_staff_detail', 'Can view staff detail')),
95 | },
96 | ),
97 | migrations.AddField(
98 | model_name='room',
99 | name='room_type',
100 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='main.RoomType'),
101 | ),
102 | migrations.AddField(
103 | model_name='reservation',
104 | name='staff',
105 | field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='main.Staff'),
106 | ),
107 | ]
108 |
--------------------------------------------------------------------------------
/main/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db import models
3 | from django.urls import reverse
4 | from django.utils import timezone
5 | from django.utils.encoding import python_2_unicode_compatible
6 |
7 |
8 | @python_2_unicode_compatible
9 | class Staff(models.Model):
10 | """ Model for staffs """
11 | profile_picture = models.ImageField(upload_to='staff_img/', default='images/staff.png')
12 | staff_id = models.AutoField(primary_key=True)
13 | first_name = models.CharField(max_length=50)
14 | middle_name = models.CharField(max_length=50, null=False, blank=True)
15 | last_name = models.CharField(max_length=50)
16 | contact_no = models.CharField(max_length=15)
17 | address = models.CharField(max_length=100)
18 | email_address = models.EmailField()
19 | user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, editable=False)
20 |
21 | class Meta:
22 | ordering = ['first_name', 'middle_name', 'last_name']
23 | permissions = (("can_view_staff", "Can view staff"), ('can_view_staff_detail', 'Can view staff detail'))
24 |
25 | def __str__(self): # Unicode support
26 | return '({0}) {1} {2}'.format(self.staff_id, self.first_name, self.last_name)
27 |
28 |
29 | @python_2_unicode_compatible
30 | class Customer(models.Model):
31 | """Model for customers"""
32 | customer_id = models.AutoField(primary_key=True)
33 | first_name = models.CharField(max_length=50)
34 | middle_name = models.CharField(max_length=50, null=False, blank=True)
35 | last_name = models.CharField(max_length=50)
36 | contact_no = models.CharField(max_length=15)
37 | address = models.CharField(max_length=100)
38 | email_address = models.EmailField(null=True, blank=True)
39 |
40 | class Meta:
41 | ordering = ['first_name', 'middle_name', 'last_name']
42 | permissions = (('can_view_customer', 'Can view customer'),)
43 |
44 | def get_absolute_url(self):
45 | """
46 | This generates the url for customer detail.
47 | 'customer-detail' is the name of the url.
48 | Takes argument customer_id
49 | """
50 | return reverse('customer-detail', args=str([self.customer_id]))
51 |
52 | def __str__(self):
53 | return '({0}) {1} {2}'.format(self.customer_id, self.first_name, self.last_name)
54 |
55 |
56 | @python_2_unicode_compatible
57 | class Reservation(models.Model):
58 | """Models for reservations"""
59 | reservation_id = models.AutoField(primary_key=True)
60 | customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
61 | staff = models.ForeignKey(Staff, on_delete=models.CASCADE, editable=False)
62 | no_of_children = models.PositiveSmallIntegerField(default=0)
63 | no_of_adults = models.PositiveSmallIntegerField(default=1)
64 | reservation_date_time = models.DateTimeField(default=timezone.now)
65 | expected_arrival_date_time = models.DateTimeField(default=timezone.now)
66 | expected_departure_date_time = models.DateTimeField(default=timezone.now)
67 |
68 | class Meta:
69 | permissions = (('can_view_reservation', 'Can view reservation'),
70 | ('can_view_reservation_detail', 'Can view reservation detail'),)
71 |
72 | def get_absolute_url(self):
73 | return reverse('reservation-detail', args=str([self.reservation_id]))
74 |
75 | def __str__(self):
76 | return '({0}) {1} {2}'.format(self.reservation_id, self.customer.first_name, self.customer.last_name)
77 |
78 |
79 | @python_2_unicode_compatible
80 | class Room(models.Model):
81 | room_no = models.CharField(max_length=10, primary_key=True)
82 | room_type = models.ForeignKey('RoomType', null=False, blank=True, on_delete=models.CASCADE)
83 | availability = models.BooleanField(default=0)
84 | reservation = models.ForeignKey(Reservation, null=True, blank=True, on_delete=models.SET_NULL)
85 | facility = models.ManyToManyField('Facility')
86 |
87 | class Meta:
88 | ordering = ['room_no', ]
89 | permissions = (('can_view_room', 'Can view room'),)
90 |
91 | def __str__(self):
92 | return "%s - %s - Rs. %i" % (self.room_no, self.room_type.name, self.room_type.price)
93 |
94 | def display_facility(self):
95 | """
96 | This function should be defined since facility is many-to-many relationship
97 | It cannot be displayed directly on the admin panel for list_display
98 | """
99 | return ', '.join([facility.name for facility in self.facility.all()])
100 |
101 | display_facility.short_description = 'Facilities'
102 |
103 | def get_absolute_url(self):
104 | return reverse('room-detail', args=[self.room_no])
105 |
106 | def save(self, *args, **kwargs): # Overriding default behaviour of save
107 | if self.reservation: # If it is reserved, than it should not be available
108 | self.availability = 0
109 | else:
110 | self.availability = 1
111 |
112 | super().save(*args, **kwargs)
113 |
114 |
115 | @python_2_unicode_compatible
116 | class Facility(models.Model):
117 | name = models.CharField(max_length=25)
118 | price = models.PositiveSmallIntegerField()
119 |
120 | class Meta:
121 | verbose_name_plural = 'Facilities' # Otherwise admin panel shows Facilitys
122 |
123 | def __str__(self):
124 | return self.name
125 |
126 |
127 | @python_2_unicode_compatible
128 | class RoomType(models.Model):
129 | name = models.CharField(max_length=25)
130 | price = models.PositiveSmallIntegerField()
131 |
132 | def __str__(self):
133 | return self.name
134 |
135 |
--------------------------------------------------------------------------------
/main/static/img/staffavatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
10 |
11 |
12 |
14 |
16 |
17 |
20 |
21 |
24 |
26 |
29 |
32 |
34 |
35 |
37 |
39 |
40 |
43 |
49 |
53 |
54 |
56 |
58 |
60 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/main/templates/reserve.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% block content-title %}
4 | {{ title }}
5 | {% endblock %}
6 | {% block main-content %}
7 |
84 | {% comment %}
85 |
90 | {% endcomment %}
91 | {% endblock %}
92 |
93 | {% block media-files %}
94 | {% load static %}
95 |
96 |
98 |
99 |
101 |
102 |
104 |
106 |
108 |
110 |
112 |
114 |
115 |
116 |
118 |
120 | {% endblock %}
121 |
--------------------------------------------------------------------------------
/main/static/js/SelectBox.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | 'use strict';
3 | var SelectBox = {
4 | cache: {},
5 | init: function(id) {
6 | var box = document.getElementById(id);
7 | var node;
8 | SelectBox.cache[id] = [];
9 | var cache = SelectBox.cache[id];
10 | var boxOptions = box.options;
11 | var boxOptionsLength = boxOptions.length;
12 | for (var i = 0, j = boxOptionsLength; i < j; i++) {
13 | node = boxOptions[i];
14 | cache.push({value: node.value, text: node.text, displayed: 1});
15 | }
16 | },
17 | redisplay: function(id) {
18 | // Repopulate HTML select box from cache
19 | var box = document.getElementById(id);
20 | var node;
21 | $(box).empty(); // clear all options
22 | var new_options = box.outerHTML.slice(0, -9); // grab just the opening tag
23 | var cache = SelectBox.cache[id];
24 | for (var i = 0, j = cache.length; i < j; i++) {
25 | node = cache[i];
26 | if (node.displayed) {
27 | var new_option = new Option(node.text, node.value, false, false);
28 | // Shows a tooltip when hovering over the option
29 | new_option.setAttribute("title", node.text);
30 | new_options += new_option.outerHTML;
31 | }
32 | }
33 | new_options += '';
34 | box.outerHTML = new_options;
35 | },
36 | filter: function(id, text) {
37 | // Redisplay the HTML select box, displaying only the choices containing ALL
38 | // the words in text. (It's an AND search.)
39 | var tokens = text.toLowerCase().split(/\s+/);
40 | var node, token;
41 | var cache = SelectBox.cache[id];
42 | for (var i = 0, j = cache.length; i < j; i++) {
43 | node = cache[i];
44 | node.displayed = 1;
45 | var node_text = node.text.toLowerCase();
46 | var numTokens = tokens.length;
47 | for (var k = 0; k < numTokens; k++) {
48 | token = tokens[k];
49 | if (node_text.indexOf(token) === -1) {
50 | node.displayed = 0;
51 | break; // Once the first token isn't found we're done
52 | }
53 | }
54 | }
55 | SelectBox.redisplay(id);
56 | },
57 | delete_from_cache: function(id, value) {
58 | var node, delete_index = null;
59 | var cache = SelectBox.cache[id];
60 | for (var i = 0, j = cache.length; i < j; i++) {
61 | node = cache[i];
62 | if (node.value === value) {
63 | delete_index = i;
64 | break;
65 | }
66 | }
67 | cache.splice(delete_index, 1);
68 | },
69 | add_to_cache: function(id, option) {
70 | SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
71 | },
72 | cache_contains: function(id, value) {
73 | // Check if an item is contained in the cache
74 | var node;
75 | var cache = SelectBox.cache[id];
76 | for (var i = 0, j = cache.length; i < j; i++) {
77 | node = cache[i];
78 | if (node.value === value) {
79 | return true;
80 | }
81 | }
82 | return false;
83 | },
84 | move: function(from, to) {
85 | var from_box = document.getElementById(from);
86 | var option;
87 | var boxOptions = from_box.options;
88 | var boxOptionsLength = boxOptions.length;
89 | for (var i = 0, j = boxOptionsLength; i < j; i++) {
90 | option = boxOptions[i];
91 | var option_value = option.value;
92 | if (option.selected && SelectBox.cache_contains(from, option_value)) {
93 | SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
94 | SelectBox.delete_from_cache(from, option_value);
95 | }
96 | }
97 | SelectBox.redisplay(from);
98 | SelectBox.redisplay(to);
99 | },
100 | move_all: function(from, to) {
101 | var from_box = document.getElementById(from);
102 | var option;
103 | var boxOptions = from_box.options;
104 | var boxOptionsLength = boxOptions.length;
105 | for (var i = 0, j = boxOptionsLength; i < j; i++) {
106 | option = boxOptions[i];
107 | var option_value = option.value;
108 | if (SelectBox.cache_contains(from, option_value)) {
109 | SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
110 | SelectBox.delete_from_cache(from, option_value);
111 | }
112 | }
113 | SelectBox.redisplay(from);
114 | SelectBox.redisplay(to);
115 | },
116 | sort: function(id) {
117 | SelectBox.cache[id].sort(function(a, b) {
118 | a = a.text.toLowerCase();
119 | b = b.text.toLowerCase();
120 | try {
121 | if (a > b) {
122 | return 1;
123 | }
124 | if (a < b) {
125 | return -1;
126 | }
127 | }
128 | catch (e) {
129 | // silently fail on IE 'unknown' exception
130 | }
131 | return 0;
132 | } );
133 | },
134 | select_all: function(id) {
135 | var box = document.getElementById(id);
136 | var boxOptions = box.options;
137 | var boxOptionsLength = boxOptions.length;
138 | for (var i = 0; i < boxOptionsLength; i++) {
139 | boxOptions[i].selected = 'selected';
140 | }
141 | }
142 | };
143 | window.SelectBox = SelectBox;
144 | })(django.jQuery);
145 |
--------------------------------------------------------------------------------
/main/static/js/actions.js:
--------------------------------------------------------------------------------
1 | /*global gettext, interpolate, ngettext*/
2 | (function($) {
3 | 'use strict';
4 | var lastChecked;
5 |
6 | $.fn.actions = function(opts) {
7 | var options = $.extend({}, $.fn.actions.defaults, opts);
8 | var actionCheckboxes = $(this);
9 | var list_editable_changed = false;
10 | var showQuestion = function() {
11 | $(options.acrossClears).hide();
12 | $(options.acrossQuestions).show();
13 | $(options.allContainer).hide();
14 | },
15 | showClear = function() {
16 | $(options.acrossClears).show();
17 | $(options.acrossQuestions).hide();
18 | $(options.actionContainer).toggleClass(options.selectedClass);
19 | $(options.allContainer).show();
20 | $(options.counterContainer).hide();
21 | },
22 | reset = function() {
23 | $(options.acrossClears).hide();
24 | $(options.acrossQuestions).hide();
25 | $(options.allContainer).hide();
26 | $(options.counterContainer).show();
27 | },
28 | clearAcross = function() {
29 | reset();
30 | $(options.acrossInput).val(0);
31 | $(options.actionContainer).removeClass(options.selectedClass);
32 | },
33 | checker = function(checked) {
34 | if (checked) {
35 | showQuestion();
36 | } else {
37 | reset();
38 | }
39 | $(actionCheckboxes).prop("checked", checked)
40 | .parent().parent().toggleClass(options.selectedClass, checked);
41 | },
42 | updateCounter = function() {
43 | var sel = $(actionCheckboxes).filter(":checked").length;
44 | // data-actions-icnt is defined in the generated HTML
45 | // and contains the total amount of objects in the queryset
46 | var actions_icnt = $('.action-counter').data('actionsIcnt');
47 | $(options.counterContainer).html(interpolate(
48 | ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
49 | sel: sel,
50 | cnt: actions_icnt
51 | }, true));
52 | $(options.allToggle).prop("checked", function() {
53 | var value;
54 | if (sel === actionCheckboxes.length) {
55 | value = true;
56 | showQuestion();
57 | } else {
58 | value = false;
59 | clearAcross();
60 | }
61 | return value;
62 | });
63 | };
64 | // Show counter by default
65 | $(options.counterContainer).show();
66 | // Check state of checkboxes and reinit state if needed
67 | $(this).filter(":checked").each(function(i) {
68 | $(this).parent().parent().toggleClass(options.selectedClass);
69 | updateCounter();
70 | if ($(options.acrossInput).val() === 1) {
71 | showClear();
72 | }
73 | });
74 | $(options.allToggle).show().click(function() {
75 | checker($(this).prop("checked"));
76 | updateCounter();
77 | });
78 | $("a", options.acrossQuestions).click(function(event) {
79 | event.preventDefault();
80 | $(options.acrossInput).val(1);
81 | showClear();
82 | });
83 | $("a", options.acrossClears).click(function(event) {
84 | event.preventDefault();
85 | $(options.allToggle).prop("checked", false);
86 | clearAcross();
87 | checker(0);
88 | updateCounter();
89 | });
90 | lastChecked = null;
91 | $(actionCheckboxes).click(function(event) {
92 | if (!event) { event = window.event; }
93 | var target = event.target ? event.target : event.srcElement;
94 | if (lastChecked && $.data(lastChecked) !== $.data(target) && event.shiftKey === true) {
95 | var inrange = false;
96 | $(lastChecked).prop("checked", target.checked)
97 | .parent().parent().toggleClass(options.selectedClass, target.checked);
98 | $(actionCheckboxes).each(function() {
99 | if ($.data(this) === $.data(lastChecked) || $.data(this) === $.data(target)) {
100 | inrange = (inrange) ? false : true;
101 | }
102 | if (inrange) {
103 | $(this).prop("checked", target.checked)
104 | .parent().parent().toggleClass(options.selectedClass, target.checked);
105 | }
106 | });
107 | }
108 | $(target).parent().parent().toggleClass(options.selectedClass, target.checked);
109 | lastChecked = target;
110 | updateCounter();
111 | });
112 | $('form#changelist-form table#result_list tr').on('change', 'td:gt(0) :input', function() {
113 | list_editable_changed = true;
114 | });
115 | $('form#changelist-form button[name="index"]').click(function(event) {
116 | if (list_editable_changed) {
117 | return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
118 | }
119 | });
120 | $('form#changelist-form input[name="_save"]').click(function(event) {
121 | var action_changed = false;
122 | $('select option:selected', options.actionContainer).each(function() {
123 | if ($(this).val()) {
124 | action_changed = true;
125 | }
126 | });
127 | if (action_changed) {
128 | if (list_editable_changed) {
129 | return confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action."));
130 | } else {
131 | return confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."));
132 | }
133 | }
134 | });
135 | };
136 | /* Setup plugin defaults */
137 | $.fn.actions.defaults = {
138 | actionContainer: "div.actions",
139 | counterContainer: "span.action-counter",
140 | allContainer: "div.actions span.all",
141 | acrossInput: "div.actions input.select-across",
142 | acrossQuestions: "div.actions span.question",
143 | acrossClears: "div.actions span.clear",
144 | allToggle: "#action-toggle",
145 | selectedClass: "selected"
146 | };
147 | $(document).ready(function() {
148 | var $actionsEls = $('tr input.action-select');
149 | if ($actionsEls.length > 0) {
150 | $actionsEls.actions();
151 | }
152 | });
153 | })(django.jQuery);
154 |
--------------------------------------------------------------------------------
/main/static/js/RelatedObjectLookups.js:
--------------------------------------------------------------------------------
1 | /*global SelectBox, interpolate*/
2 | // Handles related-objects functionality: lookup link for raw_id_fields
3 | // and Add Another links.
4 |
5 | (function($) {
6 | 'use strict';
7 |
8 | // IE doesn't accept periods or dashes in the window name, but the element IDs
9 | // we use to generate popup window names may contain them, therefore we map them
10 | // to allowed characters in a reversible way so that we can locate the correct
11 | // element when the popup window is dismissed.
12 | function id_to_windowname(text) {
13 | text = text.replace(/\./g, '__dot__');
14 | text = text.replace(/\-/g, '__dash__');
15 | return text;
16 | }
17 |
18 | function windowname_to_id(text) {
19 | text = text.replace(/__dot__/g, '.');
20 | text = text.replace(/__dash__/g, '-');
21 | return text;
22 | }
23 |
24 | function showAdminPopup(triggeringLink, name_regexp, add_popup) {
25 | var name = triggeringLink.id.replace(name_regexp, '');
26 | name = id_to_windowname(name);
27 | var href = triggeringLink.href;
28 | if (add_popup) {
29 | if (href.indexOf('?') === -1) {
30 | href += '?_popup=1';
31 | } else {
32 | href += '&_popup=1';
33 | }
34 | }
35 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
36 | win.focus();
37 | return false;
38 | }
39 |
40 | function showRelatedObjectLookupPopup(triggeringLink) {
41 | return showAdminPopup(triggeringLink, /^lookup_/, true);
42 | }
43 |
44 | function dismissRelatedLookupPopup(win, chosenId) {
45 | var name = windowname_to_id(win.name);
46 | var elem = document.getElementById(name);
47 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) {
48 | elem.value += ',' + chosenId;
49 | } else {
50 | document.getElementById(name).value = chosenId;
51 | }
52 | win.close();
53 | }
54 |
55 | function showRelatedObjectPopup(triggeringLink) {
56 | return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
57 | }
58 |
59 | function updateRelatedObjectLinks(triggeringLink) {
60 | var $this = $(triggeringLink);
61 | var siblings = $this.nextAll('.change-related, .delete-related');
62 | if (!siblings.length) {
63 | return;
64 | }
65 | var value = $this.val();
66 | if (value) {
67 | siblings.each(function() {
68 | var elm = $(this);
69 | elm.attr('href', elm.attr('data-href-template').replace('__fk__', value));
70 | });
71 | } else {
72 | siblings.removeAttr('href');
73 | }
74 | }
75 |
76 | function dismissAddRelatedObjectPopup(win, newId, newRepr) {
77 | var name = windowname_to_id(win.name);
78 | var elem = document.getElementById(name);
79 | if (elem) {
80 | var elemName = elem.nodeName.toUpperCase();
81 | if (elemName === 'SELECT') {
82 | elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
83 | } else if (elemName === 'INPUT') {
84 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) {
85 | elem.value += ',' + newId;
86 | } else {
87 | elem.value = newId;
88 | }
89 | }
90 | // Trigger a change event to update related links if required.
91 | $(elem).trigger('change');
92 | } else {
93 | var toId = name + "_to";
94 | var o = new Option(newRepr, newId);
95 | SelectBox.add_to_cache(toId, o);
96 | SelectBox.redisplay(toId);
97 | }
98 | win.close();
99 | }
100 |
101 | function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
102 | var id = windowname_to_id(win.name).replace(/^edit_/, '');
103 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
104 | var selects = $(selectsSelector);
105 | selects.find('option').each(function() {
106 | if (this.value === objId) {
107 | this.textContent = newRepr;
108 | this.value = newId;
109 | }
110 | });
111 | selects.next().find('.select2-selection__rendered').each(function() {
112 | // The element can have a clear button as a child.
113 | // Use the lastChild to modify only the displayed value.
114 | this.lastChild.textContent = newRepr;
115 | this.title = newRepr;
116 | });
117 | win.close();
118 | }
119 |
120 | function dismissDeleteRelatedObjectPopup(win, objId) {
121 | var id = windowname_to_id(win.name).replace(/^delete_/, '');
122 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
123 | var selects = $(selectsSelector);
124 | selects.find('option').each(function() {
125 | if (this.value === objId) {
126 | $(this).remove();
127 | }
128 | }).trigger('change');
129 | win.close();
130 | }
131 |
132 | // Global for testing purposes
133 | window.id_to_windowname = id_to_windowname;
134 | window.windowname_to_id = windowname_to_id;
135 |
136 | window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
137 | window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
138 | window.showRelatedObjectPopup = showRelatedObjectPopup;
139 | window.updateRelatedObjectLinks = updateRelatedObjectLinks;
140 | window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
141 | window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
142 | window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
143 |
144 | // Kept for backward compatibility
145 | window.showAddAnotherPopup = showRelatedObjectPopup;
146 | window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
147 |
148 | $(document).ready(function() {
149 | $("a[data-popup-opener]").click(function(event) {
150 | event.preventDefault();
151 | opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
152 | });
153 | $('body').on('click', '.related-widget-wrapper-link', function(e) {
154 | e.preventDefault();
155 | if (this.href) {
156 | var event = $.Event('django:show-related', {href: this.href});
157 | $(this).trigger(event);
158 | if (!event.isDefaultPrevented()) {
159 | showRelatedObjectPopup(this);
160 | }
161 | }
162 | });
163 | $('body').on('change', '.related-widget-wrapper select', function(e) {
164 | var event = $.Event('django:update-related');
165 | $(this).trigger(event);
166 | if (!event.isDefaultPrevented()) {
167 | updateRelatedObjectLinks(this);
168 | }
169 | });
170 | $('.related-widget-wrapper select').trigger('change');
171 | $('body').on('click', '.related-lookup', function(e) {
172 | e.preventDefault();
173 | var event = $.Event('django:lookup-related');
174 | $(this).trigger(event);
175 | if (!event.isDefaultPrevented()) {
176 | showRelatedObjectLookupPopup(this);
177 | }
178 | });
179 | });
180 |
181 | })(django.jQuery);
182 |
--------------------------------------------------------------------------------
/main/static/js/core.js:
--------------------------------------------------------------------------------
1 | // Core javascript helper functions
2 |
3 | // basic browser identification & version
4 | var isOpera = (navigator.userAgent.indexOf("Opera") >= 0) && parseFloat(navigator.appVersion);
5 | var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]);
6 |
7 | // quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]);
8 | function quickElement() {
9 | 'use strict';
10 | var obj = document.createElement(arguments[0]);
11 | if (arguments[2]) {
12 | var textNode = document.createTextNode(arguments[2]);
13 | obj.appendChild(textNode);
14 | }
15 | var len = arguments.length;
16 | for (var i = 3; i < len; i += 2) {
17 | obj.setAttribute(arguments[i], arguments[i + 1]);
18 | }
19 | arguments[1].appendChild(obj);
20 | return obj;
21 | }
22 |
23 | // "a" is reference to an object
24 | function removeChildren(a) {
25 | 'use strict';
26 | while (a.hasChildNodes()) {
27 | a.removeChild(a.lastChild);
28 | }
29 | }
30 |
31 | // ----------------------------------------------------------------------------
32 | // Find-position functions by PPK
33 | // See http://www.quirksmode.org/js/findpos.html
34 | // ----------------------------------------------------------------------------
35 | function findPosX(obj) {
36 | 'use strict';
37 | var curleft = 0;
38 | if (obj.offsetParent) {
39 | while (obj.offsetParent) {
40 | curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft);
41 | obj = obj.offsetParent;
42 | }
43 | // IE offsetParent does not include the top-level
44 | if (isIE && obj.parentElement) {
45 | curleft += obj.offsetLeft - obj.scrollLeft;
46 | }
47 | } else if (obj.x) {
48 | curleft += obj.x;
49 | }
50 | return curleft;
51 | }
52 |
53 | function findPosY(obj) {
54 | 'use strict';
55 | var curtop = 0;
56 | if (obj.offsetParent) {
57 | while (obj.offsetParent) {
58 | curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop);
59 | obj = obj.offsetParent;
60 | }
61 | // IE offsetParent does not include the top-level
62 | if (isIE && obj.parentElement) {
63 | curtop += obj.offsetTop - obj.scrollTop;
64 | }
65 | } else if (obj.y) {
66 | curtop += obj.y;
67 | }
68 | return curtop;
69 | }
70 |
71 | //-----------------------------------------------------------------------------
72 | // Date object extensions
73 | // ----------------------------------------------------------------------------
74 | (function() {
75 | 'use strict';
76 | Date.prototype.getTwelveHours = function() {
77 | var hours = this.getHours();
78 | if (hours === 0) {
79 | return 12;
80 | }
81 | else {
82 | return hours <= 12 ? hours : hours - 12;
83 | }
84 | };
85 |
86 | Date.prototype.getTwoDigitMonth = function() {
87 | return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1);
88 | };
89 |
90 | Date.prototype.getTwoDigitDate = function() {
91 | return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate();
92 | };
93 |
94 | Date.prototype.getTwoDigitTwelveHour = function() {
95 | return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours();
96 | };
97 |
98 | Date.prototype.getTwoDigitHour = function() {
99 | return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours();
100 | };
101 |
102 | Date.prototype.getTwoDigitMinute = function() {
103 | return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes();
104 | };
105 |
106 | Date.prototype.getTwoDigitSecond = function() {
107 | return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
108 | };
109 |
110 | Date.prototype.getHourMinute = function() {
111 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute();
112 | };
113 |
114 | Date.prototype.getHourMinuteSecond = function() {
115 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond();
116 | };
117 |
118 | Date.prototype.getFullMonthName = function() {
119 | return typeof window.CalendarNamespace === "undefined"
120 | ? this.getTwoDigitMonth()
121 | : window.CalendarNamespace.monthsOfYear[this.getMonth()];
122 | };
123 |
124 | Date.prototype.strftime = function(format) {
125 | var fields = {
126 | B: this.getFullMonthName(),
127 | c: this.toString(),
128 | d: this.getTwoDigitDate(),
129 | H: this.getTwoDigitHour(),
130 | I: this.getTwoDigitTwelveHour(),
131 | m: this.getTwoDigitMonth(),
132 | M: this.getTwoDigitMinute(),
133 | p: (this.getHours() >= 12) ? 'PM' : 'AM',
134 | S: this.getTwoDigitSecond(),
135 | w: '0' + this.getDay(),
136 | x: this.toLocaleDateString(),
137 | X: this.toLocaleTimeString(),
138 | y: ('' + this.getFullYear()).substr(2, 4),
139 | Y: '' + this.getFullYear(),
140 | '%': '%'
141 | };
142 | var result = '', i = 0;
143 | while (i < format.length) {
144 | if (format.charAt(i) === '%') {
145 | result = result + fields[format.charAt(i + 1)];
146 | ++i;
147 | }
148 | else {
149 | result = result + format.charAt(i);
150 | }
151 | ++i;
152 | }
153 | return result;
154 | };
155 |
156 | // ----------------------------------------------------------------------------
157 | // String object extensions
158 | // ----------------------------------------------------------------------------
159 | String.prototype.pad_left = function(pad_length, pad_string) {
160 | var new_string = this;
161 | for (var i = 0; new_string.length < pad_length; i++) {
162 | new_string = pad_string + new_string;
163 | }
164 | return new_string;
165 | };
166 |
167 | String.prototype.strptime = function(format) {
168 | var split_format = format.split(/[.\-/]/);
169 | var date = this.split(/[.\-/]/);
170 | var i = 0;
171 | var day, month, year;
172 | while (i < split_format.length) {
173 | switch (split_format[i]) {
174 | case "%d":
175 | day = date[i];
176 | break;
177 | case "%m":
178 | month = date[i] - 1;
179 | break;
180 | case "%Y":
181 | year = date[i];
182 | break;
183 | case "%y":
184 | year = date[i];
185 | break;
186 | }
187 | ++i;
188 | }
189 | // Create Date object from UTC since the parsed value is supposed to be
190 | // in UTC, not local time. Also, the calendar uses UTC functions for
191 | // date extraction.
192 | return new Date(Date.UTC(year, month, day));
193 | };
194 |
195 | })();
196 | // ----------------------------------------------------------------------------
197 | // Get the computed style for and element
198 | // ----------------------------------------------------------------------------
199 | function getStyle(oElm, strCssRule) {
200 | 'use strict';
201 | var strValue = "";
202 | if(document.defaultView && document.defaultView.getComputedStyle) {
203 | strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
204 | }
205 | else if(oElm.currentStyle) {
206 | strCssRule = strCssRule.replace(/\-(\w)/g, function(strMatch, p1) {
207 | return p1.toUpperCase();
208 | });
209 | strValue = oElm.currentStyle[strCssRule];
210 | }
211 | return strValue;
212 | }
213 |
--------------------------------------------------------------------------------
/main/static/js/calendar.js:
--------------------------------------------------------------------------------
1 | /*global gettext, pgettext, get_format, quickElement, removeChildren*/
2 | /*
3 | calendar.js - Calendar functions by Adrian Holovaty
4 | depends on core.js for utility functions like removeChildren or quickElement
5 | */
6 |
7 | (function() {
8 | 'use strict';
9 | // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions
10 | var CalendarNamespace = {
11 | monthsOfYear: [
12 | gettext('January'),
13 | gettext('February'),
14 | gettext('March'),
15 | gettext('April'),
16 | gettext('May'),
17 | gettext('June'),
18 | gettext('July'),
19 | gettext('August'),
20 | gettext('September'),
21 | gettext('October'),
22 | gettext('November'),
23 | gettext('December')
24 | ],
25 | daysOfWeek: [
26 | pgettext('one letter Sunday', 'S'),
27 | pgettext('one letter Monday', 'M'),
28 | pgettext('one letter Tuesday', 'T'),
29 | pgettext('one letter Wednesday', 'W'),
30 | pgettext('one letter Thursday', 'T'),
31 | pgettext('one letter Friday', 'F'),
32 | pgettext('one letter Saturday', 'S')
33 | ],
34 | firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')),
35 | isLeapYear: function(year) {
36 | return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0));
37 | },
38 | getDaysInMonth: function(month, year) {
39 | var days;
40 | if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) {
41 | days = 31;
42 | }
43 | else if (month === 4 || month === 6 || month === 9 || month === 11) {
44 | days = 30;
45 | }
46 | else if (month === 2 && CalendarNamespace.isLeapYear(year)) {
47 | days = 29;
48 | }
49 | else {
50 | days = 28;
51 | }
52 | return days;
53 | },
54 | draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999
55 | var today = new Date();
56 | var todayDay = today.getDate();
57 | var todayMonth = today.getMonth() + 1;
58 | var todayYear = today.getFullYear();
59 | var todayClass = '';
60 |
61 | // Use UTC functions here because the date field does not contain time
62 | // and using the UTC function variants prevent the local time offset
63 | // from altering the date, specifically the day field. For example:
64 | //
65 | // ```
66 | // var x = new Date('2013-10-02');
67 | // var day = x.getDate();
68 | // ```
69 | //
70 | // The day variable above will be 1 instead of 2 in, say, US Pacific time
71 | // zone.
72 | var isSelectedMonth = false;
73 | if (typeof selected !== 'undefined') {
74 | isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month);
75 | }
76 |
77 | month = parseInt(month);
78 | year = parseInt(year);
79 | var calDiv = document.getElementById(div_id);
80 | removeChildren(calDiv);
81 | var calTable = document.createElement('table');
82 | quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year);
83 | var tableBody = quickElement('tbody', calTable);
84 |
85 | // Draw days-of-week header
86 | var tableRow = quickElement('tr', tableBody);
87 | for (var i = 0; i < 7; i++) {
88 | quickElement('th', tableRow, CalendarNamespace.daysOfWeek[(i + CalendarNamespace.firstDayOfWeek) % 7]);
89 | }
90 |
91 | var startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay();
92 | var days = CalendarNamespace.getDaysInMonth(month, year);
93 |
94 | var nonDayCell;
95 |
96 | // Draw blanks before first of month
97 | tableRow = quickElement('tr', tableBody);
98 | for (i = 0; i < startingPos; i++) {
99 | nonDayCell = quickElement('td', tableRow, ' ');
100 | nonDayCell.className = "nonday";
101 | }
102 |
103 | function calendarMonth(y, m) {
104 | function onClick(e) {
105 | e.preventDefault();
106 | callback(y, m, this.textContent);
107 | }
108 | return onClick;
109 | }
110 |
111 | // Draw days of month
112 | var currentDay = 1;
113 | for (i = startingPos; currentDay <= days; i++) {
114 | if (i % 7 === 0 && currentDay !== 1) {
115 | tableRow = quickElement('tr', tableBody);
116 | }
117 | if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) {
118 | todayClass = 'today';
119 | } else {
120 | todayClass = '';
121 | }
122 |
123 | // use UTC function; see above for explanation.
124 | if (isSelectedMonth && currentDay === selected.getUTCDate()) {
125 | if (todayClass !== '') {
126 | todayClass += " ";
127 | }
128 | todayClass += "selected";
129 | }
130 |
131 | var cell = quickElement('td', tableRow, '', 'class', todayClass);
132 | var link = quickElement('a', cell, currentDay, 'href', '#');
133 | link.addEventListener('click', calendarMonth(year, month));
134 | currentDay++;
135 | }
136 |
137 | // Draw blanks after end of month (optional, but makes for valid code)
138 | while (tableRow.childNodes.length < 7) {
139 | nonDayCell = quickElement('td', tableRow, ' ');
140 | nonDayCell.className = "nonday";
141 | }
142 |
143 | calDiv.appendChild(calTable);
144 | }
145 | };
146 |
147 | // Calendar -- A calendar instance
148 | function Calendar(div_id, callback, selected) {
149 | // div_id (string) is the ID of the element in which the calendar will
150 | // be displayed
151 | // callback (string) is the name of a JavaScript function that will be
152 | // called with the parameters (year, month, day) when a day in the
153 | // calendar is clicked
154 | this.div_id = div_id;
155 | this.callback = callback;
156 | this.today = new Date();
157 | this.currentMonth = this.today.getMonth() + 1;
158 | this.currentYear = this.today.getFullYear();
159 | if (typeof selected !== 'undefined') {
160 | this.selected = selected;
161 | }
162 | }
163 | Calendar.prototype = {
164 | drawCurrent: function() {
165 | CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected);
166 | },
167 | drawDate: function(month, year, selected) {
168 | this.currentMonth = month;
169 | this.currentYear = year;
170 |
171 | if(selected) {
172 | this.selected = selected;
173 | }
174 |
175 | this.drawCurrent();
176 | },
177 | drawPreviousMonth: function() {
178 | if (this.currentMonth === 1) {
179 | this.currentMonth = 12;
180 | this.currentYear--;
181 | }
182 | else {
183 | this.currentMonth--;
184 | }
185 | this.drawCurrent();
186 | },
187 | drawNextMonth: function() {
188 | if (this.currentMonth === 12) {
189 | this.currentMonth = 1;
190 | this.currentYear++;
191 | }
192 | else {
193 | this.currentMonth++;
194 | }
195 | this.drawCurrent();
196 | },
197 | drawPreviousYear: function() {
198 | this.currentYear--;
199 | this.drawCurrent();
200 | },
201 | drawNextYear: function() {
202 | this.currentYear++;
203 | this.drawCurrent();
204 | }
205 | };
206 | window.Calendar = Calendar;
207 | window.CalendarNamespace = CalendarNamespace;
208 | })();
209 |
--------------------------------------------------------------------------------
/main/static/css/widgets.css:
--------------------------------------------------------------------------------
1 | /* SELECTOR (FILTER INTERFACE) */
2 |
3 | .selector {
4 | width: 800px;
5 | float: left;
6 | }
7 |
8 | .selector select {
9 | width: 380px;
10 | height: 17.2em;
11 | }
12 |
13 | .selector-available, .selector-chosen {
14 | float: left;
15 | width: 380px;
16 | text-align: center;
17 | margin-bottom: 5px;
18 | }
19 |
20 | .selector-chosen select {
21 | border-top: none;
22 | }
23 |
24 | .selector-available h2, .selector-chosen h2 {
25 | border: 1px solid #ccc;
26 | border-radius: 4px 4px 0 0;
27 | }
28 |
29 | .selector-chosen h2 {
30 | background: #79aec8;
31 | color: #fff;
32 | }
33 |
34 | .selector .selector-available h2 {
35 | background: #f8f8f8;
36 | color: #666;
37 | }
38 |
39 | .selector .selector-filter {
40 | background: white;
41 | border: 1px solid #ccc;
42 | border-width: 0 1px;
43 | padding: 8px;
44 | color: #999;
45 | font-size: 10px;
46 | margin: 0;
47 | text-align: left;
48 | }
49 |
50 | .selector .selector-filter label,
51 | .inline-group .aligned .selector .selector-filter label {
52 | float: left;
53 | margin: 7px 0 0;
54 | width: 18px;
55 | height: 18px;
56 | padding: 0;
57 | overflow: hidden;
58 | line-height: 1;
59 | }
60 |
61 | .selector .selector-available input {
62 | width: 320px;
63 | margin-left: 8px;
64 | }
65 |
66 | .selector ul.selector-chooser {
67 | float: left;
68 | width: 22px;
69 | background-color: #eee;
70 | border-radius: 10px;
71 | margin: 10em 5px 0 5px;
72 | padding: 0;
73 | }
74 |
75 | .selector-chooser li {
76 | margin: 0;
77 | padding: 3px;
78 | list-style-type: none;
79 | }
80 |
81 | .selector select {
82 | padding: 0 10px;
83 | margin: 0 0 10px;
84 | border-radius: 0 0 4px 4px;
85 | }
86 |
87 | .selector-add, .selector-remove {
88 | width: 16px;
89 | height: 16px;
90 | display: block;
91 | text-indent: -3000px;
92 | overflow: hidden;
93 | cursor: default;
94 | opacity: 0.3;
95 | }
96 |
97 | .active.selector-add, .active.selector-remove {
98 | opacity: 1;
99 | }
100 |
101 | .active.selector-add:hover, .active.selector-remove:hover {
102 | cursor: pointer;
103 | }
104 |
105 | .selector-add {
106 | background: url(../img/selector-icons.svg) 0 -96px no-repeat;
107 | }
108 |
109 | .active.selector-add:focus, .active.selector-add:hover {
110 | background-position: 0 -112px;
111 | }
112 |
113 | .selector-remove {
114 | background: url(../img/selector-icons.svg) 0 -64px no-repeat;
115 | }
116 |
117 | .active.selector-remove:focus, .active.selector-remove:hover {
118 | background-position: 0 -80px;
119 | }
120 |
121 | a.selector-chooseall, a.selector-clearall {
122 | display: inline-block;
123 | height: 16px;
124 | text-align: left;
125 | margin: 1px auto 3px;
126 | overflow: hidden;
127 | font-weight: bold;
128 | line-height: 16px;
129 | color: #666;
130 | text-decoration: none;
131 | opacity: 0.3;
132 | }
133 |
134 | a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
135 | a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
136 | color: #447e9b;
137 | }
138 |
139 | a.active.selector-chooseall, a.active.selector-clearall {
140 | opacity: 1;
141 | }
142 |
143 | a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
144 | cursor: pointer;
145 | }
146 |
147 | a.selector-chooseall {
148 | padding: 0 18px 0 0;
149 | background: url(../img/selector-icons.svg) right -160px no-repeat;
150 | cursor: default;
151 | }
152 |
153 | a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
154 | background-position: 100% -176px;
155 | }
156 |
157 | a.selector-clearall {
158 | padding: 0 0 0 18px;
159 | background: url(../img/selector-icons.svg) 0 -128px no-repeat;
160 | cursor: default;
161 | }
162 |
163 | a.active.selector-clearall:focus, a.active.selector-clearall:hover {
164 | background-position: 0 -144px;
165 | }
166 |
167 | /* STACKED SELECTORS */
168 |
169 | .stacked {
170 | float: left;
171 | width: 490px;
172 | }
173 |
174 | .stacked select {
175 | width: 480px;
176 | height: 10.1em;
177 | }
178 |
179 | .stacked .selector-available, .stacked .selector-chosen {
180 | width: 480px;
181 | }
182 |
183 | .stacked .selector-available {
184 | margin-bottom: 0;
185 | }
186 |
187 | .stacked .selector-available input {
188 | width: 422px;
189 | }
190 |
191 | .stacked ul.selector-chooser {
192 | height: 22px;
193 | width: 50px;
194 | margin: 0 0 10px 40%;
195 | background-color: #eee;
196 | border-radius: 10px;
197 | }
198 |
199 | .stacked .selector-chooser li {
200 | float: left;
201 | padding: 3px 3px 3px 5px;
202 | }
203 |
204 | .stacked .selector-chooseall, .stacked .selector-clearall {
205 | display: none;
206 | }
207 |
208 | .stacked .selector-add {
209 | background: url(../img/selector-icons.svg) 0 -32px no-repeat;
210 | cursor: default;
211 | }
212 |
213 | .stacked .active.selector-add {
214 | background-position: 0 -48px;
215 | cursor: pointer;
216 | }
217 |
218 | .stacked .selector-remove {
219 | background: url(../img/selector-icons.svg) 0 0 no-repeat;
220 | cursor: default;
221 | }
222 |
223 | .stacked .active.selector-remove {
224 | background-position: 0 -16px;
225 | cursor: pointer;
226 | }
227 |
228 | .selector .help-icon {
229 | background: url(../img/icon-unknown.svg) 0 0 no-repeat;
230 | display: inline-block;
231 | vertical-align: middle;
232 | margin: -2px 0 0 2px;
233 | width: 13px;
234 | height: 13px;
235 | }
236 |
237 | .selector .selector-chosen .help-icon {
238 | background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
239 | }
240 |
241 | .selector .search-label-icon {
242 | background: url(../img/search.svg) 0 0 no-repeat;
243 | display: inline-block;
244 | height: 18px;
245 | width: 18px;
246 | }
247 |
248 |
249 | /* DATE AND TIME */
250 |
251 | p.datetime {
252 | line-height: 20px;
253 | margin: 0;
254 | padding: 0;
255 | /*color: #666;*/
256 | /* font-weight: bold;*/
257 | }
258 |
259 | .datetime span {
260 | white-space: nowrap;
261 | font-weight: normal;
262 | /*font-size: 11px;*/
263 | /*color: #ccc;*/
264 | }
265 |
266 |
267 | .datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
268 | min-width: 0;
269 | margin-left: 5px;
270 | margin-bottom: 4px;
271 | }
272 |
273 | table p.datetime {
274 | font-size: 11px;
275 | margin-left: 0;
276 | padding-left: 0;
277 | }
278 |
279 |
280 | .datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
281 | position: relative;
282 | display: inline-block;
283 | vertical-align: middle;
284 | height: 16px;
285 | width: 16px;
286 | overflow: hidden;
287 | }
288 |
289 | .datetimeshortcuts .clock-icon {
290 | background: url(../img/icon-clock.svg) 0 0 no-repeat;
291 | }
292 |
293 | .datetimeshortcuts a:focus .clock-icon,
294 | .datetimeshortcuts a:hover .clock-icon {
295 | background-position: 0 -16px;
296 | }
297 |
298 | .datetimeshortcuts .date-icon {
299 | background: url(../img/icon-calendar.svg) 0 0 no-repeat;
300 | top: -1px;
301 | }
302 |
303 | .datetimeshortcuts a:focus .date-icon,
304 | .datetimeshortcuts a:hover .date-icon {
305 | background-position: 0 -16px;
306 | }
307 |
308 | .timezonewarning {
309 | font-size: 11px;
310 | color: #999;
311 | }
312 |
313 | /* CALENDARS & CLOCKS */
314 |
315 | .calendarbox, .clockbox {
316 | margin: 5px auto;
317 | font-size: 12px;
318 | width: 19em;
319 | text-align: center;
320 | background: white;
321 | border: 1px solid #ddd;
322 | border-radius: 4px;
323 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
324 | overflow: hidden;
325 | position: relative;
326 | }
327 |
328 | .clockbox {
329 | width: auto;
330 | }
331 |
332 | .calendar {
333 | margin: 0;
334 | padding: 0;
335 | }
336 |
337 | .calendar table {
338 | margin: 0;
339 | padding: 0;
340 | border-collapse: collapse;
341 | background: white;
342 | width: 100%;
343 | }
344 |
345 | .calendar caption, .calendarbox h2 {
346 | margin: 0;
347 | text-align: center;
348 | border-top: none;
349 | background: #f5dd5d;
350 | font-weight: 700;
351 | font-size: 12px;
352 | color: #333;
353 | }
354 |
355 | .calendar th {
356 | padding: 8px 5px;
357 | background: #f8f8f8;
358 | border-bottom: 1px solid #ddd;
359 | font-weight: 400;
360 | font-size: 12px;
361 | text-align: center;
362 | color: #666;
363 | }
364 |
365 | .calendar td {
366 | font-weight: 400;
367 | font-size: 12px;
368 | text-align: center;
369 | padding: 0;
370 | border-top: 1px solid #eee;
371 | border-bottom: none;
372 | }
373 |
374 | .calendar td.selected a {
375 | background: #79aec8;
376 | color: #fff;
377 | }
378 |
379 | .calendar td.nonday {
380 | background: #f8f8f8;
381 | }
382 |
383 | .calendar td.today a {
384 | font-weight: 700;
385 | }
386 |
387 | .calendar td a, .timelist a {
388 | display: block;
389 | font-weight: 400;
390 | padding: 6px;
391 | text-decoration: none;
392 | color: #444;
393 | }
394 |
395 | .calendar td a:focus, .timelist a:focus,
396 | .calendar td a:hover, .timelist a:hover {
397 | background: #79aec8;
398 | color: white;
399 | }
400 |
401 | .calendar td a:active, .timelist a:active {
402 | background: #417690;
403 | color: white;
404 | }
405 |
406 | .calendarnav {
407 | font-size: 10px;
408 | text-align: center;
409 | color: #ccc;
410 | margin: 0;
411 | padding: 1px 3px;
412 | }
413 |
414 | .calendarnav a:link, #calendarnav a:visited,
415 | #calendarnav a:focus, #calendarnav a:hover {
416 | color: #999;
417 | }
418 |
419 | .calendar-shortcuts {
420 | background: white;
421 | font-size: 11px;
422 | line-height: 11px;
423 | border-top: 1px solid #eee;
424 | padding: 8px 0;
425 | color: #ccc;
426 | }
427 |
428 | .calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
429 | display: block;
430 | position: absolute;
431 | top: 8px;
432 | width: 15px;
433 | height: 15px;
434 | text-indent: -9999px;
435 | padding: 0;
436 | }
437 |
438 | .calendarnav-previous {
439 | left: 10px;
440 | background: url(../img/calendar-icons.svg) 0 0 no-repeat;
441 | }
442 |
443 | .calendarbox .calendarnav-previous:focus,
444 | .calendarbox .calendarnav-previous:hover {
445 | background-position: 0 -15px;
446 | }
447 |
448 | .calendarnav-next {
449 | right: 10px;
450 | background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
451 | }
452 |
453 | .calendarbox .calendarnav-next:focus,
454 | .calendarbox .calendarnav-next:hover {
455 | background-position: 0 -45px;
456 | }
457 |
458 | .calendar-cancel {
459 | margin: 0;
460 | padding: 4px 0;
461 | font-size: 12px;
462 | background: #eee;
463 | border-top: 1px solid #ddd;
464 | color: #333;
465 | }
466 |
467 | .calendar-cancel:focus, .calendar-cancel:hover {
468 | background: #ddd;
469 | }
470 |
471 | .calendar-cancel a {
472 | color: black;
473 | display: block;
474 | }
475 |
476 | ul.timelist, .timelist li {
477 | list-style-type: none;
478 | margin: 0;
479 | padding: 0;
480 | }
481 |
482 | .timelist a {
483 | padding: 2px;
484 | }
485 |
--------------------------------------------------------------------------------
/main/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ title }}
12 |
13 |
14 |
16 |
17 |
20 |
21 | {% load static %}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Hotel Management System
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {% block navbar-items %}
43 | {% if user.is_authenticated %}
44 |
45 | Profile
46 |
47 | {% endif %}
48 |
49 | Help
50 |
51 | {% endblock %}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {% block sidebar %}
62 |
63 |
125 | {% endblock %}
126 |
127 |
128 |
129 |
131 |
132 | {% block content-title %}
133 | {{ title }}
134 | {% endblock %}
135 |
136 |
137 |
138 | {% block main-content %}
139 |
141 | {% endblock %}
142 |
143 |
144 |
145 | {% block pagination %}
146 |
147 | {% if is_paginated %}
148 |
149 |
173 |
174 | {% endif %}
175 |
176 | {% endblock %}
177 |
178 |
179 |
180 |
181 |
182 |
184 |
185 |
188 |
191 |
192 | {% load static %}
193 |
194 | {% block media-files %}
195 | {% endblock %}
196 |
204 | {% block additional-javascript %}
205 | {% endblock %}
206 |
207 |
208 |
--------------------------------------------------------------------------------
/main/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import permission_required
2 | from django.contrib.auth.mixins import PermissionRequiredMixin
3 | from django.contrib.auth.models import User, Group
4 | from django.core.exceptions import ValidationError
5 | from django.db import transaction, IntegrityError
6 | from django.db.models import Q
7 | from django.http import Http404
8 | from django.shortcuts import render, redirect, get_object_or_404 # For displaying in template
9 | from django.urls import reverse_lazy
10 | from django.utils import timezone
11 | from django.utils.translation import ugettext_lazy as _
12 | from django.views import generic
13 |
14 | from .forms import Signup, ReservationForm, CheckInRequestForm
15 | from .models import Room, Reservation, Customer, Staff # Import Models
16 |
17 |
18 | def index(request):
19 | """
20 | This is the view for homepage.
21 | This is a function based view.
22 | """
23 | page_title = _("Hotel Management System") # For page title as well as heading
24 | total_num_rooms = Room.objects.all().count()
25 | available_num_rooms = Room.objects.exclude(reservation__isnull=False).count()
26 | total_num_reservations = Reservation.objects.all().count()
27 | total_num_staffs = Staff.objects.all().count()
28 | total_num_customers = Customer.objects.all().count()
29 | if total_num_reservations == 0:
30 | last_reserved_by = Reservation.objects.none()
31 | else:
32 | last_reserved_by = Reservation.objects.get_queryset().latest('reservation_date_time')
33 |
34 | return render(
35 | request,
36 | 'index.html',
37 | # context is whatever sent to the template.
38 | # the index of the dictionary i.e. title in 'title': page_title
39 | # is used as variable in templates
40 | # where as the next one is the variable of this function
41 | {
42 | 'title': page_title,
43 | 'total_num_rooms': total_num_rooms,
44 | 'available_num_rooms': available_num_rooms,
45 | 'total_num_reservations': total_num_reservations,
46 | 'total_num_staffs': total_num_staffs,
47 | 'total_num_customers': total_num_customers,
48 | 'last_reserved_by': last_reserved_by,
49 | }
50 | )
51 |
52 |
53 | @transaction.atomic
54 | def signup(request):
55 | title = "Signup"
56 | if request.user.is_authenticated:
57 | request.session.flush()
58 | if request.method == 'POST':
59 | form = Signup(request.POST)
60 | if form.is_valid():
61 | try:
62 | with transaction.atomic():
63 | staffs_group = get_object_or_404(Group, name__iexact="Staffs")
64 | form.save()
65 | staff_id = form.cleaned_data['staff_id']
66 | username = form.cleaned_data['username']
67 | s = get_object_or_404(Staff, staff_id__exact=staff_id)
68 | s.user = get_object_or_404(User, username__iexact=username)
69 | s.user.set_password(form.cleaned_data['password1'])
70 | s.user.groups.add(staffs_group)
71 | s.user.save()
72 | s.save()
73 | except IntegrityError:
74 | raise Http404
75 | return redirect('index')
76 | else:
77 | form = Signup()
78 |
79 | return render(
80 | request,
81 | 'signup.html', {
82 | 'form': form, 'title': title},
83 | )
84 |
85 |
86 | @permission_required('main.add_reservation', 'login', raise_exception=True)
87 | @transaction.atomic
88 | def reserve(request):
89 | title = "Add Reservation"
90 | reservation = Reservation.objects.none()
91 | if request.method == 'POST':
92 | reservation_form = ReservationForm(request.POST)
93 | if reservation_form.is_valid():
94 | try:
95 | with transaction.atomic():
96 | customer = Customer(
97 | first_name=reservation_form.cleaned_data.get('first_name'),
98 | middle_name=reservation_form.cleaned_data.get('middle_name'),
99 | last_name=reservation_form.cleaned_data.get('last_name'),
100 | email_address=reservation_form.cleaned_data.get('email'),
101 | contact_no=reservation_form.cleaned_data.get('contact_no'),
102 | address=reservation_form.cleaned_data.get('address'),
103 | )
104 | customer.save()
105 | staff = request.user
106 | reservation = Reservation(
107 | staff=get_object_or_404(Staff, user=staff),
108 | customer=customer,
109 | no_of_children=reservation_form.cleaned_data.get('no_of_children'),
110 | no_of_adults=reservation_form.cleaned_data.get('no_of_adults'),
111 | expected_arrival_date_time=reservation_form.cleaned_data.get('expected_arrival_date_time'),
112 | expected_departure_date_time=reservation_form.cleaned_data.get('expected_departure_date_time'),
113 | reservation_date_time=timezone.now(),
114 | )
115 | reservation.save()
116 | for room in reservation_form.cleaned_data.get('rooms'):
117 | room.reservation = reservation
118 | room.save()
119 | except IntegrityError:
120 | raise Http404
121 | return render(
122 | request,
123 | 'reserve_success.html', {
124 | 'reservation': reservation,
125 | }
126 | )
127 | else:
128 | reservation_form = ReservationForm()
129 |
130 | return render(
131 | request,
132 | 'reserve.html', {
133 | 'title': title,
134 | 'reservation_form': reservation_form,
135 | }
136 | )
137 |
138 |
139 | def reserve_success(request):
140 | pass
141 |
142 |
143 | # For generic ListView or DetailView, the default templates should be stored in templates/{{app_name}}/{{template_name}}
144 | # By default template_name = modelName_list || modelName_detail.
145 | # eg room_list, room_detail
146 | # @permission_required('main.can_view_staff')
147 | class RoomListView(PermissionRequiredMixin, generic.ListView):
148 | """
149 | View for list of rooms.
150 | Implements generic ListView.
151 | """
152 | model = Room # Chooses the model for listing objects
153 | paginate_by = 5 # By how many objects this has to be paginated
154 | title = _("Room List") # This is used for title and heading
155 | permission_required = 'main.can_view_room'
156 |
157 | # By default only objects of the model are sent as context
158 | # However extra context can be passed using field extra_context
159 | # Here title is passed.
160 |
161 | extra_context = {'title': title}
162 |
163 | # By default:
164 | # template_name = room_list
165 | # if you want to change it, use field template_name
166 | # here don't do this, since it is already done as default.
167 | # for own views, it can be done.
168 |
169 | def get_queryset(self):
170 | filter_value = self.request.GET.get('filter', 'all')
171 | if filter_value == 'all':
172 | filter_value = 0
173 | elif filter_value == 'avail':
174 | filter_value = 1
175 | try:
176 | new_context = Room.objects.filter(availability__in=[filter_value, 1])
177 | except ValidationError:
178 | raise Http404(_("Wrong filter argument given."))
179 | return new_context
180 |
181 | def get_context_data(self, **kwargs):
182 | context = super(RoomListView, self).get_context_data(**kwargs)
183 | context['filter'] = self.request.GET.get('filter', 'all')
184 | return context
185 |
186 |
187 | class RoomDetailView(PermissionRequiredMixin, generic.DetailView):
188 | """
189 | View for detail of room
190 | Implements generic DetailView
191 | """
192 | # The remaining are same as previous.
193 | model = Room
194 | title = _("Room Information")
195 | permission_required = 'main.can_view_room'
196 | extra_context = {'title': title}
197 |
198 |
199 | class ReservationListView(PermissionRequiredMixin, generic.ListView, generic.FormView):
200 | """
201 | View for list of reservations.
202 | Implements generic ListView.
203 | """
204 | model = Reservation
205 | # queryset field selects the objects to be displayed by the query.
206 | # Here, the objects are displayed by reservation date time in descending order
207 | queryset = Reservation.objects.all().order_by('-reservation_date_time')
208 | title = _("Reservation List")
209 | paginate_by = 5
210 | allow_empty = True
211 | form_class = CheckInRequestForm
212 | success_url = reverse_lazy('check_in-list')
213 | permission_required = 'main.can_view_reservation'
214 | extra_context = {'title': title}
215 |
216 | @transaction.atomic
217 | def form_valid(self, form):
218 | try:
219 | with transaction.atomic():
220 | checkin = form.save(commit=False)
221 | checkin.user = self.request.user
222 | checkin.save()
223 | except IntegrityError:
224 | raise Http404
225 | return super().form_valid(form)
226 |
227 |
228 | class ReservationDetailView(PermissionRequiredMixin, generic.DetailView):
229 | """
230 | View for detail of reservation
231 | Implements generic DetailView
232 | """
233 | model = Reservation
234 | title = _("Reservation Information")
235 | permission_required = 'main.can_view_reservation'
236 | raise_exception = True
237 | extra_context = {'title': title}
238 |
239 |
240 | class CustomerDetailView(PermissionRequiredMixin, generic.DetailView):
241 | """
242 | View for detail of customer
243 | Implements generic DetailView
244 | """
245 | model = Customer
246 | title = _("Customer Information")
247 | permission_required = 'main.can_view_customer'
248 | raise_exception = True
249 | extra_context = {'title': title}
250 |
251 |
252 | class StaffDetailView(PermissionRequiredMixin, generic.DetailView):
253 | """
254 | View for detail of staff
255 | Implements generic DetailView
256 | """
257 | model = Staff
258 | title = _("Staff Information")
259 | permission_required = 'main.can_view_staff_detail'
260 | extra_context = {'title': title}
261 |
262 |
263 | class ProfileView(generic.TemplateView):
264 | template_name = 'profile.html'
265 | title = "Profile"
266 | extra_context = {'title': title}
267 |
268 | def get_context_data(self, **kwargs):
269 | context = super().get_context_data(**kwargs)
270 | if self.request.user.is_authenticated:
271 | context['information'] = get_object_or_404(Staff, user=self.request.user)
272 | context['user_information'] = self.request.user
273 | else:
274 | raise Http404("Your are not logged in.")
275 | return context
276 |
277 |
278 | class GuestListView(PermissionRequiredMixin, generic.ListView):
279 | """
280 | View for list of guests present in hotel.
281 | """
282 | model = Customer
283 | paginate_by = 5
284 | allow_empty = True
285 | queryset = Customer.objects.all().filter(Q(reservation__checkin__isnull=False),
286 | Q(reservation__checkin__checkout__isnull=True))
287 | permission_required = 'main.can_view_customer'
288 | template_name = 'main/guest_list.html'
289 | title = 'Guest List View'
290 | context_object_name = 'guest_list'
291 | extra_context = {'title': title}
292 |
--------------------------------------------------------------------------------
/main/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib.auth.models import User
3 | from django.utils.translation import ugettext_lazy as _
4 |
5 | from payment.models import CheckIn
6 | from .models import Staff, Room
7 | from .widgets import MySplitDateTime, FilteredSelectMultiple
8 |
9 |
10 | class Signup(forms.Form):
11 | """
12 | This is the signup form.
13 | """
14 | error_messages = {
15 | 'password_mismatch': _("The two password fields didn't match."),
16 | 'id_not_found': _("This ID is not available."),
17 | 'info_not_matched': _("The information didn't match."),
18 | 'username_exists': _("The username already exists."),
19 | 'staff_username_exists': _("This staff already has an account please login to it."),
20 | }
21 |
22 | staff_id = forms.IntegerField(
23 | label=_('ID'),
24 | help_text=_("Enter your staff ID"),
25 | widget=forms.NumberInput(
26 | attrs={
27 | 'class': 'form-control',
28 | 'placeholder': _('Enter your ID'),
29 | }
30 | )
31 | )
32 | first_name = forms.CharField(
33 | label=_("First Name"),
34 | max_length=50,
35 | widget=forms.TextInput(
36 | attrs={
37 | 'class': 'form-control',
38 | 'placeholder': _('Enter your first name'),
39 | }
40 | )
41 | )
42 | middle_name = forms.CharField(
43 | label=_('Middle Name'),
44 | required=False,
45 | max_length=50,
46 | widget=forms.TextInput(
47 | attrs={
48 | 'class': 'form-control',
49 | 'placeholder': _('Enter your middle name'),
50 | }
51 | )
52 | )
53 | last_name = forms.CharField(
54 | label=_("Last Name"),
55 | max_length=50,
56 | widget=forms.TextInput(
57 | attrs={
58 | 'class': 'form-control',
59 | 'placeholder': _('Enter your last name'),
60 | }
61 | )
62 | )
63 | contact_no = forms.CharField(
64 | label=_('Contact No'),
65 | max_length=15,
66 | widget=forms.TextInput(
67 | attrs={
68 | 'class': 'form-control',
69 | 'placeholder': _('Enter your contact number'),
70 | }
71 | )
72 | )
73 | email = forms.EmailField(
74 | label=_("Email"),
75 | max_length=50,
76 | widget=forms.EmailInput(
77 | attrs={
78 | 'class': 'form-control',
79 | 'placeholder': _('Enter your email'),
80 | }
81 | )
82 | )
83 | username = forms.CharField(
84 | label=_("Username"),
85 | max_length=32,
86 | widget=forms.TextInput(
87 | attrs={
88 | 'class': 'form-control',
89 | 'placeholder': _('Username'),
90 | }
91 | )
92 | )
93 | password1 = forms.CharField(
94 | label=_("Password"),
95 | widget=forms.PasswordInput(
96 | attrs={
97 | 'class': 'form-control',
98 | 'placeholder': _('Password')
99 | }
100 | )
101 | )
102 | password2 = forms.CharField(
103 | label=_("Password confirmation"),
104 | widget=forms.PasswordInput(
105 | attrs={
106 | 'class': 'form-control',
107 | 'placeholder': _('Confirm Password')
108 | }
109 | ),
110 | help_text=_("Enter the same password as above, for verification."))
111 |
112 | def clean(self):
113 | staff_id = self.cleaned_data.get('staff_id')
114 | first_name = self.cleaned_data.get('first_name')
115 | middle_name = self.cleaned_data.get('middle_name')
116 | last_name = self.cleaned_data.get('last_name')
117 | contact_no = self.cleaned_data.get('contact_no')
118 | email = self.cleaned_data.get('email')
119 | username = self.cleaned_data.get('username')
120 | password1 = self.cleaned_data.get("password1")
121 | password2 = self.cleaned_data.get("password2")
122 | s = Staff.objects.filter(staff_id__exact=staff_id)
123 | u = User.objects.filter(username__iexact=username)
124 | if s.count():
125 | st = Staff.objects.get(staff_id__exact=staff_id)
126 | if st.user:
127 | raise forms.ValidationError(
128 | self.error_messages['staff_username_exists'],
129 | code='staff_username_exits',
130 | )
131 | elif first_name != st.first_name or middle_name != st.middle_name or last_name != st.last_name or email != st.email_address or contact_no != st.contact_no:
132 | raise forms.ValidationError(
133 | self.error_messages['info_not_matched'],
134 | code='info_not_matched',
135 | )
136 | else:
137 | raise forms.ValidationError(
138 | self.error_messages['id_not_found'],
139 | code='id_not_found',
140 | )
141 |
142 | if u.count():
143 | raise forms.ValidationError(
144 | self.error_messages['username_exists'],
145 | code='username_exists',
146 | )
147 |
148 | if password1 and password2 and password1 != password2:
149 | raise forms.ValidationError(
150 | self.error_messages['password_mismatch'],
151 | code='password_mismatch',
152 | )
153 |
154 | def save(self):
155 | user = User.objects.create(
156 | username=self.cleaned_data['username'],
157 | first_name=self.cleaned_data['first_name'],
158 | last_name=self.cleaned_data['last_name'],
159 | email=self.cleaned_data['email'],
160 | )
161 | return user
162 |
163 |
164 | class ReservationForm(forms.Form):
165 | """
166 | This is the form for reservation.
167 | """
168 | error_messages = {
169 | 'date_time_error': 'Departure time earlier than Arrival time',
170 | }
171 | first_name = forms.CharField(
172 | label=_("First Name"),
173 | max_length=50,
174 | widget=forms.TextInput(
175 | attrs={
176 | 'class': 'form-control',
177 | 'placeholder': _('Enter first name'),
178 | }
179 | )
180 | )
181 | middle_name = forms.CharField(
182 | label=_('Middle Name'),
183 | required=False,
184 | max_length=50,
185 | widget=forms.TextInput(
186 | attrs={
187 | 'class': 'form-control',
188 | 'placeholder': _('Enter middle name'),
189 | }
190 | )
191 | )
192 | last_name = forms.CharField(
193 | label=_("Last Name"),
194 | max_length=50,
195 | widget=forms.TextInput(
196 | attrs={
197 | 'class': 'form-control',
198 | 'placeholder': _('Enter last name'),
199 | }
200 | )
201 | )
202 | contact_no = forms.CharField(
203 | label=_('Contact No'),
204 | max_length=15,
205 | widget=forms.TextInput(
206 | attrs={
207 | 'class': 'form-control',
208 | 'placeholder': _('Enter contact number'),
209 | }
210 | )
211 | )
212 | email = forms.EmailField(
213 | label=_("Email"),
214 | max_length=50,
215 | required=False,
216 | widget=forms.EmailInput(
217 | attrs={
218 | 'class': 'form-control',
219 | 'placeholder': _('Enter email'),
220 | }
221 | )
222 | )
223 | address = forms.CharField(
224 | label=_("Address"),
225 | max_length=50,
226 | widget=forms.TextInput(
227 | attrs={
228 | 'class': 'form-control',
229 | 'placeholder': _('Enter address'),
230 | }
231 | )
232 | )
233 | no_of_children = forms.IntegerField(
234 | widget=forms.NumberInput(
235 | attrs={
236 | 'class': 'form-control',
237 | 'placeholder': _('Enter number of children'),
238 | }
239 | )
240 | )
241 | no_of_adults = forms.IntegerField(
242 | widget=forms.NumberInput(
243 | attrs={
244 | 'class': 'form-control',
245 | 'placeholder': _('Enter number of adults'),
246 | }
247 | )
248 | )
249 | rooms = forms.ModelMultipleChoiceField(
250 | queryset=Room.objects.filter(reservation__isnull=True),
251 | widget=FilteredSelectMultiple(
252 | is_stacked=True,
253 | verbose_name="Rooms",
254 | attrs={
255 | 'class': 'form-control',
256 | },
257 | ),
258 | )
259 | expected_arrival_date_time = forms.SplitDateTimeField(
260 | widget=MySplitDateTime(
261 | )
262 | )
263 |
264 | expected_departure_date_time = forms.SplitDateTimeField(
265 | widget=MySplitDateTime(
266 | )
267 | )
268 |
269 |
270 | class CheckInRequestForm(forms.ModelForm):
271 | class Meta:
272 | model = CheckIn
273 | fields = ['reservation']
274 | widgets = {'reservation': forms.HiddenInput()}
275 |
276 |
277 | '''
278 | def clean_expected_arrival_date_time(self):
279 | expected_arrival_date_time = ' '.join(self.cleaned_data.get('expected_arrival_date_time'))
280 | expected_arrival_date_time = parse_datetime(expected_arrival_date_time)
281 | expected_arrival_date_time = pytz.timezone(TIME_ZONE).localize(expected_arrival_date_time, is_dst=None)
282 | return expected_arrival_date_time
283 |
284 | def clean_expected_departure_date_time(self):
285 | expected_departure_date_time = ' '.join(self.cleaned_data.get('expected_departure_date_time'))
286 | expected_departure_date_time = parse_datetime(expected_departure_date_time)
287 | expected_departure_date_time = pytz.timezone(TIME_ZONE).localize(expected_departure_date_time, is_dst=None)
288 | return expected_departure_date_time
289 | '''
290 |
291 | """
292 | class ReservationForm(forms.ModelForm):
293 | error_messages = {
294 | 'number_error': 'Enter the number properly',
295 | 'date_error': 'Departure should be after arrival',
296 | }
297 | first_name = forms.CharField(
298 | label=_("First Name"),
299 | max_length=50,
300 | widget=forms.TextInput()
301 | )
302 | middle_name = forms.CharField(
303 | label=_('Middle Name'),
304 | required=False,
305 | max_length=50,
306 | widget=forms.TextInput()
307 | )
308 | last_name = forms.CharField(
309 | label=_("Last Name"),
310 | max_length=50,
311 | widget=forms.TextInput()
312 | )
313 | contact_no = forms.CharField(
314 | label=_('Contact No'),
315 | max_length=15,
316 | widget=forms.TextInput()
317 | )
318 | email = forms.EmailField(
319 | label=_("Email"),
320 | max_length=50,
321 | widget=forms.EmailInput()
322 | )
323 | address = forms.CharField(
324 | label=_("Address"),
325 | max_length=50,
326 | widget=forms.TextInput()
327 | )
328 |
329 | class Meta:
330 | model = Reservation
331 | fields = (
332 | 'no_of_children',
333 | 'no_of_adults',
334 | 'expected_arrival_date_time',
335 | 'expected_departure_date_time',
336 | )
337 |
338 | def clean_no_of_children(self):
339 | if self.cleaned_data.get('no_of_children') < 0:
340 | raise forms.ValidationError(
341 | self.error_messages['number_error'],
342 | code='number_error',
343 | )
344 |
345 | def clean_no_of_adults(self):
346 | if self.cleaned_data.get('no_of_adults') <= 0:
347 | raise forms.ValidationError(
348 | self.error_messages['number_error'],
349 | code='number_error',
350 | )
351 |
352 | def clean_expected_departure_date_time(self):
353 | if self.cleaned_data.get('expected_departure_date_time') <= self.cleaned_data.get('expected_arrival_date_time'):
354 | raise forms.ValidationError(
355 | self.error_messages['date_error'],
356 | code='date_error',
357 | )
358 |
359 | def clean(self):
360 | pass
361 |
362 | """
363 |
--------------------------------------------------------------------------------
/main/static/js/SelectFilter2.js:
--------------------------------------------------------------------------------
1 | /*global SelectBox, gettext, interpolate, quickElement, SelectFilter*/
2 | /*
3 | SelectFilter2 - Turns a multiple-select box into a filter interface.
4 |
5 | Requires jQuery, core.js, and SelectBox.js.
6 | */
7 | (function($) {
8 | 'use strict';
9 | function findForm(node) {
10 | // returns the node of the form containing the given node
11 | if (node.tagName.toLowerCase() !== 'form') {
12 | return findForm(node.parentNode);
13 | }
14 | return node;
15 | }
16 |
17 | window.SelectFilter = {
18 | init: function(field_id, field_name, is_stacked) {
19 | if (field_id.match(/__prefix__/)) {
20 | // Don't initialize on empty forms.
21 | return;
22 | }
23 | var from_box = document.getElementById(field_id);
24 | from_box.id += '_from'; // change its ID
25 | from_box.className = 'filtered';
26 |
27 | var ps = from_box.parentNode.getElementsByTagName('p');
28 | for (var i = 0; i < ps.length; i++) {
29 | if (ps[i].className.indexOf("info") !== -1) {
30 | // Remove , because it just gets in the way.
31 | from_box.parentNode.removeChild(ps[i]);
32 | } else if (ps[i].className.indexOf("help") !== -1) {
33 | // Move help text up to the top so it isn't below the select
34 | // boxes or wrapped off on the side to the right of the add
35 | // button:
36 | from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);
37 | }
38 | }
39 |
40 | //
or
41 | var selector_div = quickElement('div', from_box.parentNode);
42 | selector_div.className = is_stacked ? 'selector stacked' : 'selector';
43 |
44 | //
45 | var selector_available = quickElement('div', selector_div);
46 | selector_available.className = 'selector-available';
47 | var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
48 | quickElement(
49 | 'span', title_available, '',
50 | 'class', 'help help-tooltip help-icon',
51 | 'title', interpolate(
52 | gettext(
53 | 'This is the list of available %s. You may choose some by ' +
54 | 'selecting them in the box below and then clicking the ' +
55 | '"Choose" arrow between the two boxes.'
56 | ),
57 | [field_name]
58 | )
59 | );
60 |
61 | var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
62 | filter_p.className = 'selector-filter';
63 |
64 | var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input');
65 |
66 | quickElement(
67 | 'span', search_filter_label, '',
68 | 'class', 'help-tooltip search-label-icon',
69 | 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
70 | );
71 |
72 | filter_p.appendChild(document.createTextNode(' '));
73 |
74 | var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
75 | filter_input.id = field_id + '_input';
76 |
77 | selector_available.appendChild(from_box);
78 | var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link');
79 | choose_all.className = 'selector-chooseall';
80 |
81 | //
82 | var selector_chooser = quickElement('ul', selector_div);
83 | selector_chooser.className = 'selector-chooser';
84 | var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
85 | add_link.className = 'selector-add';
86 | var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
87 | remove_link.className = 'selector-remove';
88 |
89 | //
90 | var selector_chosen = quickElement('div', selector_div);
91 | selector_chosen.className = 'selector-chosen';
92 | var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
93 | quickElement(
94 | 'span', title_chosen, '',
95 | 'class', 'help help-tooltip help-icon',
96 | 'title', interpolate(
97 | gettext(
98 | 'This is the list of chosen %s. You may remove some by ' +
99 | 'selecting them in the box below and then clicking the ' +
100 | '"Remove" arrow between the two boxes.'
101 | ),
102 | [field_name]
103 | )
104 | );
105 |
106 | var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
107 | to_box.className = 'filtered';
108 | var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
109 | clear_all.className = 'selector-clearall';
110 |
111 | from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
112 |
113 | // Set up the JavaScript event handlers for the select box filter interface
114 | var move_selection = function(e, elem, move_func, from, to) {
115 | if (elem.className.indexOf('active') !== -1) {
116 | move_func(from, to);
117 | SelectFilter.refresh_icons(field_id);
118 | }
119 | e.preventDefault();
120 | };
121 | choose_all.addEventListener('click', function(e) {
122 | move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
123 | });
124 | add_link.addEventListener('click', function(e) {
125 | move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
126 | });
127 | remove_link.addEventListener('click', function(e) {
128 | move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
129 | });
130 | clear_all.addEventListener('click', function(e) {
131 | move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from');
132 | });
133 | filter_input.addEventListener('keypress', function(e) {
134 | SelectFilter.filter_key_press(e, field_id);
135 | });
136 | filter_input.addEventListener('keyup', function(e) {
137 | SelectFilter.filter_key_up(e, field_id);
138 | });
139 | filter_input.addEventListener('keydown', function(e) {
140 | SelectFilter.filter_key_down(e, field_id);
141 | });
142 | selector_div.addEventListener('change', function(e) {
143 | if (e.target.tagName === 'SELECT') {
144 | SelectFilter.refresh_icons(field_id);
145 | }
146 | });
147 | selector_div.addEventListener('dblclick', function(e) {
148 | if (e.target.tagName === 'OPTION') {
149 | if (e.target.closest('select').id === field_id + '_to') {
150 | SelectBox.move(field_id + '_to', field_id + '_from');
151 | } else {
152 | SelectBox.move(field_id + '_from', field_id + '_to');
153 | }
154 | SelectFilter.refresh_icons(field_id);
155 | }
156 | });
157 | findForm(from_box).addEventListener('submit', function() {
158 | SelectBox.select_all(field_id + '_to');
159 | });
160 | SelectBox.init(field_id + '_from');
161 | SelectBox.init(field_id + '_to');
162 | // Move selected from_box options to to_box
163 | SelectBox.move(field_id + '_from', field_id + '_to');
164 |
165 | if (!is_stacked) {
166 | // In horizontal mode, give the same height to the two boxes.
167 | var j_from_box = $(from_box);
168 | var j_to_box = $(to_box);
169 | var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); };
170 | if (j_from_box.outerHeight() > 0) {
171 | resize_filters(); // This fieldset is already open. Resize now.
172 | } else {
173 | // This fieldset is probably collapsed. Wait for its 'show' event.
174 | j_to_box.closest('fieldset').one('show.fieldset', resize_filters);
175 | }
176 | }
177 |
178 | // Initial icon refresh
179 | SelectFilter.refresh_icons(field_id);
180 | },
181 | any_selected: function(field) {
182 | var any_selected = false;
183 | try {
184 | // Temporarily add the required attribute and check validity.
185 | // This is much faster in WebKit browsers than the fallback.
186 | field.attr('required', 'required');
187 | any_selected = field.is(':valid');
188 | field.removeAttr('required');
189 | } catch (e) {
190 | // Browsers that don't support :valid (IE < 10)
191 | any_selected = field.find('option:selected').length > 0;
192 | }
193 | return any_selected;
194 | },
195 | refresh_icons: function(field_id) {
196 | var from = $('#' + field_id + '_from');
197 | var to = $('#' + field_id + '_to');
198 | // Active if at least one item is selected
199 | $('#' + field_id + '_add_link').toggleClass('active', SelectFilter.any_selected(from));
200 | $('#' + field_id + '_remove_link').toggleClass('active', SelectFilter.any_selected(to));
201 | // Active if the corresponding box isn't empty
202 | $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
203 | $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
204 | },
205 | filter_key_press: function(event, field_id) {
206 | var from = document.getElementById(field_id + '_from');
207 | // don't submit form if user pressed Enter
208 | if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) {
209 | from.selectedIndex = 0;
210 | SelectBox.move(field_id + '_from', field_id + '_to');
211 | from.selectedIndex = 0;
212 | event.preventDefault();
213 | return false;
214 | }
215 | },
216 | filter_key_up: function(event, field_id) {
217 | var from = document.getElementById(field_id + '_from');
218 | var temp = from.selectedIndex;
219 | SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
220 | from.selectedIndex = temp;
221 | return true;
222 | },
223 | filter_key_down: function(event, field_id) {
224 | var from = document.getElementById(field_id + '_from');
225 | // right arrow -- move across
226 | if ((event.which && event.which === 39) || (event.keyCode && event.keyCode === 39)) {
227 | var old_index = from.selectedIndex;
228 | SelectBox.move(field_id + '_from', field_id + '_to');
229 | from.selectedIndex = (old_index === from.length) ? from.length - 1 : old_index;
230 | return false;
231 | }
232 | // down arrow -- wrap around
233 | if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) {
234 | from.selectedIndex = (from.length === from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
235 | }
236 | // up arrow -- wrap around
237 | if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) {
238 | from.selectedIndex = (from.selectedIndex === 0) ? from.length - 1 : from.selectedIndex - 1;
239 | }
240 | return true;
241 | }
242 | };
243 |
244 | window.addEventListener('load', function(e) {
245 | $('select.selectfilter, select.selectfilterstacked').each(function() {
246 | var $el = $(this),
247 | data = $el.data();
248 | SelectFilter.init($el.attr('id'), data.fieldName, parseInt(data.isStacked, 10));
249 | });
250 | });
251 |
252 | })(django.jQuery);
253 |
--------------------------------------------------------------------------------