會議室預定(小項目)
該項目仍舊是用Django框架完成的,此項目的重點在於前端頁面中有關預定的操作
首先建表,這里用的表較少,一共三張表,表結構如下:
from django.db import models class UserInfo(models.Model): name = models.CharField(verbose_name='用戶姓名', max_length=32) password = models.CharField(verbose_name='密碼', max_length=32) class MeetingRoom(models.Model): title = models.CharField(verbose_name='會議室', max_length=32) class Booking(models.Model): user = models.ForeignKey(verbose_name='用戶', to='UserInfo') room = models.ForeignKey(verbose_name='會議室', to='MeetingRoom') booking_date = models.DateField(verbose_name='預定日期') time_choices = ( (1, '8:00'), (2, '9:00'), (3, '10:00'), (4, '11:00'), (5, '12:00'), (6, '13:00'), (7, '14:00'), (8, '15:00'), (9, '16:00'), (10, '17:00'), (11, '18:00'), (12, '19:00'), (13, '20:00'), ) booking_time = models.IntegerField(verbose_name='預定時間段', choices=time_choices) class Meta: unique_together = ( ('booking_date', 'booking_time', 'room') )
接下來分配路由(項目較為簡單,所以並沒有寫注冊的頁面,這里是直接將用戶數據錄入數據庫了,若想使項目更完善,可自行添加注冊功能。)
from django.conf.urls import url from django.contrib import admin from meet import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^login/$', views.login), url(r'^index/$', views.index), url(r'^booking/$', views.booking), url(r'^log_out/$', views.log_out), ]
然后是靜態文件static的配置
STATIC_URL = '/static/' STATICFILES_DIRS=[ os.path.join(BASE_DIR, 'meet','static'),#別名所指的實際文件夾路徑 ]
這里我們用到兩個插件,分別是datetimepicker和sweetalert2,前者是在前端頁面對Date進行擴展的時間工具,后者是對alert進行美化的一共工具,如不想使用后者,直接用alert即可。
從網上下載兩個插件,放入static下。

登錄、注銷功能
url(r'^login/$', views.login), url(r'^log_out/$', views.log_out),
#注銷功能 def log_out(request): del request.session['user_info'] return redirect('/index/') def login(request): """ 用戶登錄 """ if request.method == "GET": form = LoginForm() return render(request, 'login.html', {'form': form}) else: form = LoginForm(request.POST) if form.is_valid(): rmb = form.cleaned_data.pop('rmb')#一周免登陸選項 user = models.UserInfo.objects.filter(**form.cleaned_data).first() if user: request.session['user_info'] = {'id': user.id, 'name': user.name} if rmb:#若勾選了一周免登陸選項 request.session.set_expiry(60 * 60 * 24 * 30) return redirect('/index/') else: form.add_error('password', '密碼錯誤') return render(request, 'login.html', {'form': form}) else: return render(request, 'login.html', {'form': form})
上面用到了form組件如下:
from django.forms import Form from django.forms import fields from django.forms import widgets class LoginForm(Form): name = fields.CharField( required=True, error_messages={'required': '用戶名不能為空'}, widget=widgets.TextInput(attrs={'class': 'form-control', 'placeholder': '用戶名', 'id': 'name'}) ) password = fields.CharField( required=True, error_messages={'required': '密碼不能為空'}, widget=widgets.PasswordInput(attrs={'class': 'form-control', 'placeholder': '密碼', 'id': 'password'}) ) #一周免登陸選項 rmb = fields.BooleanField(required=False, widget=widgets.CheckboxInput(attrs={'value': 1}))
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}"> <style> </style> </head> <body> <div style="width: 500px;margin: 50px auto;padding-top: 180px;"> <form class="form-horizontal" method="post" novalidate> {% csrf_token %} <div class="form-group"> <label for="name" class="col-sm-2 control-label">用戶名:</label> <div class="col-sm-10"> {{ form.name }} {{ form.errors.name.0 }} </div> </div> <div class="form-group"> <label for="password" class="col-sm-2 control-label">密碼:</label> <div class="col-sm-10"> {{ form.password }} {{ form.errors.password.0 }} </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> <label> {{ form.rmb }} 一周內免登錄 </label> </div> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-primary">登錄</button> </div> </div> </form> </div> </body> </html>
之后用於驗證登陸與否的裝飾器:
#驗證登陸與否的裝飾器 def auth(func): def inner(request, *args, **kwargs): user_info = request.session.get('user_info') if not user_info: return redirect('/login/') return func(request, *args, **kwargs) return inner
登錄功能較為簡單,不做詳述,接下來我們做首頁
我們的預定功能就在首頁中,所以首頁是重中之重。
難點:index.html中的js:tbody的生成、datetimepicker插件的使用、前后端發送的時間格式的轉換、后端錄入數據庫的操作
url(r'^index/$', views.index), url(r'^booking/$', views.booking),
#views.py中:
import json
import datetime
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse
from meet import models
from meet.form import *
from django.db.models import Q
from django.db.utils import IntegrityError
@auth def index(request): """ 會議室預定首頁 :param request: :return: """ #拿到所有的時間段 time_choices = models.Booking.time_choices user_info = request.session.get('user_info') name=user_info['name'] return render(request, 'index.html', {'time_choices': time_choices,'name':name})
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'datetimepicker/bootstrap-datetimepicker.min.css' %}"> <link rel="stylesheet" href="{% static 'sweetalert2/sweetalert2.css' %}"> {# <link rel="stylesheet" href="{% static 'mycss/index.css' %}">#} <style> body { font-size: 14px; } .shade { position: fixed; z-index: 1040; top: 0; left: 0; right: 0; bottom: 0; background-color: #999; filter: alpha(opacity=50); -moz-opacity: 0.5; opacity: 0.5; } .loading { position: fixed; z-index: 1050; top: 40%; left: 50%; height: 32px; width: 32px; margin: 0 0 0 -16px; background: url(/static/img/loading.gif); } .clearfix{ padding: 10px 0; } .input-group{ width: 230px; float:left; } .save-btn{ padding: 0 5px;float: left } table > tbody td { height: 80px; width: 80px; text-align: center; vertical-align: middle; } table > tbody td.chosen { background-color: #ebccd1; } table > tbody td.selected { background-color:#d58512 ; } .mycolor{ background-color: #EEE685; } .unable{ color: #002a80; opacity: 0.5; } </style> </head> <body> <div class="container"> <div class="panel panel-primary"> <div class="panel-heading"> <h1 class="text-center">會議室預定</h1> </div> <div class="panel-body"> <div class="clearfix"> <div style="float: left;color: red" id="errors"></div> <div class='input-group'> {# 時間插件#} <input type='text' class="form-control" id='datetimepicker11' placeholder="請選擇日期"/> <span class="input-group-addon"> <span class="glyphicon glyphicon-calendar"> </span> </span> </div> <div class="save-btn"> <a id="save" class="btn btn-primary">保存</a> </div> <div class="pull-right"> <b>hello {{ name }} </b> <a href="/log_out/">注銷</a> </div> </div> <table class="table table-bordered table-striped" style="border:1px solid red"> <thead> <tr> <th>會議室</th> {# 拿到從后端發過來的所有時間段#} {% for choice in time_choices %} <th>{{ choice.1 }}</th> {% endfor %} </tr> </thead> <tbody id="tBody"> {# tbody中的內容包含未預定信息和預定信息,且需要實時更新,所以這里用后端傳遞的方式獲取#} </tbody> </table> </div> </div> </div> <!-- 遮罩層開始 --> <div id='shade' class='shade hide'></div> <!-- 遮罩層結束 --> <!-- 加載層開始 --> <div id='loading' class='loading hide'></div> <!-- 加載層結束 --> <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script> <script src="{% static 'js/jquery.cookie.js' %}"></script> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.js' %}"></script> <script src="{% static 'datetimepicker/bootstrap-datetimepicker.min.js' %}"></script> <script src="{% static 'datetimepicker/bootstrap-datetimepicker.zh-CN.js' %}"></script> <script src="{% static 'sweetalert2/sweetalert2.js' %}"></script> <script> //插件中自帶,直接復制粘貼: // 對Date的擴展,將 Date 轉化為指定格式的String // 月(M)、日(d)、小時(h)、分(m)、秒(s)、季度(q) 可以用 1-2 個占位符, // 年(y)可以用 1-4 個占位符,毫秒(S)只能用 1 個占位符(是 1-3 位的數字) // 例子: // (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 // (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18 Date.prototype.Format = function (fmt) { //author: meizz var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小時 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; }; //自定義的全局變量: SELECTED_ROOM = {del: {}, add: {}}; CHOSEN_DATE = new Date().Format('yyyy-MM-dd');//轉成字符串格式后的今日日期 //網頁加載完成后執行的js腳本內容: $(function () { initDatepicker();//初始化日期插件 {# 初始化房間信息,將今日日期發給后端,利用ajax從后台獲得房間預訂信息#} initBookingInfo(new Date().Format('yyyy-MM-dd')); bindTdEvent();//綁定預定會議室事件 bindSaveEvent();//保存按鈕 }); //處理csrftoken: function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken')); } } }); //初始化日期插件內容: function initDatepicker() { $('#datetimepicker11').datetimepicker({ minView: "month",//最小可視是到月份,即最小選擇是到day language: "zh-CN", sideBySide: true, format: 'yyyy-mm-dd', bootcssVer: 3,//bootstrap3必寫 startDate: new Date(),//起始日為今日 autoclose: true,//自動關閉,不需要可刪 }).on('changeDate', changeDate);//綁定改日期后的事件 } //綁定的改日期后的事件: function changeDate(ev) { CHOSEN_DATE = ev.date.Format('yyyy-MM-dd');//日期變為選擇后的日期 initBookingInfo(CHOSEN_DATE);//初始化預定信息 } //初始化房間信息(利用ajax從后台獲得房間預訂信息) function initBookingInfo(date) { SELECTED_ROOM = {del: {}, add: {}}; $('#shade,#loading').removeClass('hide');//遮罩層 $.ajax({ url: '/booking/', type: 'get', data: {date: date},//字符串轉義后的今日日期 dataType: 'JSON', success: function (arg) { $('#shade,#loading').addClass('hide');//遮罩層去除 if (arg.code === 1000) {//表示后台操作成功 $('#tBody').empty(); $.each(arg.data, function (i, item) { var tr = document.createElement('tr');//此為js操作,等同於jQuery的$('<tr>') $.each(item, function (j, row) { var td = $('<td>'); $(td).text(row.text).attr('class','everytd'); $.each(row.attrs, function (k, v) { $(td).attr(k, v); }); if (row.chosen) { $(td).addClass('chosen'); } $(tr).append(td) }); $('#tBody').append(tr); }) } else { alert(arg.msg); } }, error: function () { $('#shade,#loading').addClass('hide'); alert('請求異常'); } }) } /* 綁定預定會議室事件,事件委派 */ function bindTdEvent() { $('#tBody').on('click', 'td[time-id][disable!="true"]', function () { var roomId = $(this).attr('room-id'); var timeId = $(this).attr('time-id'); //var item = {'roomId': $(this).attr('room-id'), 'timeId': $(this).attr('time-id')}; // 取消原來的預定: if ($(this).hasClass('chosen')) { $(this).removeClass('chosen').empty(); //SELECTED_ROOM['del'].push(item); if (SELECTED_ROOM.del[roomId]) { SELECTED_ROOM.del[roomId].push(timeId); } else { SELECTED_ROOM.del[roomId] = [timeId]; } } else if ($(this).hasClass('selected')) { $(this).removeClass('selected'); // 取消選擇 var timeIndex = SELECTED_ROOM.add[roomId].indexOf(timeId); if (timeIndex !== -1) { SELECTED_ROOM.add[roomId].splice(timeIndex, 1); } } else { $(this).addClass('selected'); // 選擇 if (SELECTED_ROOM.add[roomId]) { SELECTED_ROOM.add[roomId].push(timeId); } else { SELECTED_ROOM.add[roomId] = [timeId]; } } }) } /* 保存按鈕 */ function bindSaveEvent() { $('#errors').text(''); $('#save').click(function () { $('#shade,#loading').removeClass('hide'); $.ajax({ url: '/booking/', type: 'POST', data: {date: CHOSEN_DATE, data: JSON.stringify(SELECTED_ROOM)}, dataType: 'JSON', success: function (arg) { $('#shade,#loading').addClass('hide'); if (arg.code === 1000) { initBookingInfo(CHOSEN_DATE); } else { $('#errors').text(arg.msg); } swal( '保存成功', '會議室預定狀態已刷新', 'success' ) } }); }); } //鼠標懸浮變色功能(美化) $(document).ready(function(){ $('body').on('mouseover','.everytd',function () { $(this).addClass('mycolor') }) $('body').on('mouseout','.everytd',function () { $(this).removeClass('mycolor') }) }); </script> </body> </html>
#裝飾器 def auth_json(func): def inner(request, *args, **kwargs): user_info = request.session.get('user_info') if not user_info: return JsonResponse({'status': False, 'msg': '用戶未登錄'}) return func(request, *args, **kwargs) return inner @auth_json def booking(request): """ 獲取會議室預定情況以及預定會議室 :param request: :param date: :return: """ ret = {'code': 1000, 'msg': None, 'data': None} current_date = datetime.datetime.now().date()#年月日 if request.method == "GET": try: fetch_date = request.GET.get('date')#拿到前端傳過來的轉義過的字符串格式的日期 fetch_date = datetime.datetime.strptime(fetch_date, '%Y-%m-%d').date()#轉義成時間格式 if fetch_date < current_date: raise Exception('放下過往,着眼當下') #拿到當日的預定信息 booking_list = models.Booking.objects.filter(booking_date=fetch_date).select_related('user','room').order_by('booking_time') booking_dict = {}#構建方便查詢的大字典 for item in booking_list:#item是每一個預定對象 if item.room_id not in booking_dict:#對象的room_id沒在字典內 booking_dict[item.room_id] = {item.booking_time: {'name': item.user.name, 'id': item.user.id}} else:#對象的room_id在字典內 if item.booking_time not in booking_dict[item.room_id]:#但是還有預定信息沒在字典內 booking_dict[item.room_id][item.booking_time] = {'name': item.user.name, 'id': item.user.id} """ { room_id:{ time_id:{'user.name':esfsdfdsf,'user.id':1}, time_id:{'user.name':esfsdfdsf,'user.id':1}, time_id:{'user.name':esfsdfdsf,'user.id':1}, } } """ room_list = models.MeetingRoom.objects.all()#數組【所有房間對象】 booking_info = [] for room in room_list: temp = [{'text': room.title, 'attrs': {'rid': room.id}, 'chosen': False}] for choice in models.Booking.time_choices: v = {'text': '', 'attrs': {'time-id': choice[0], 'room-id': room.id}, 'chosen': False} if room.id in booking_dict and choice[0] in booking_dict[room.id]:#說明已有預定信息 v['text'] = booking_dict[room.id][choice[0]]['name']#預訂人名 v['chosen'] = True if booking_dict[room.id][choice[0]]['id'] != request.session['user_info']['id']: v['attrs']['disable'] = 'true' v['attrs']['class'] = 'unable'#不可對別人預定的房間進行操作 temp.append(v) booking_info.append(temp) ret['data'] = booking_info except Exception as e: ret['code'] = 1001 ret['msg'] = str(e) return JsonResponse(ret) else: try: #拿到預定的日期並進行轉義 booking_date = request.POST.get('date') booking_date = datetime.datetime.strptime(booking_date, '%Y-%m-%d').date() if booking_date < current_date: raise Exception('放下過往,着眼當下') #SELECTED_ROOM = {del: {roomId:timeId}, add: {roomId:timeId}}; booking_info = json.loads(request.POST.get('data')) for room_id, time_id_list in booking_info['add'].items(): if room_id not in booking_info['del']: continue for time_id in list(time_id_list): #同時點了增加和刪除,即用戶在選擇之后反悔了。。 if time_id in booking_info['del'][room_id]: booking_info['del'][room_id].remove(time_id) booking_info['add'][room_id].remove(time_id) add_booking_list = [] for room_id, time_id_list in booking_info['add'].items(): for time_id in time_id_list: obj = models.Booking( user_id=request.session['user_info']['id'], room_id=room_id, booking_time=time_id, booking_date=booking_date ) add_booking_list.append(obj) models.Booking.objects.bulk_create(add_booking_list)#批量添加,增加數據庫效率 remove_booking = Q() for room_id, time_id_list in booking_info['del'].items(): for time_id in time_id_list: temp = Q() temp.connector = 'AND' temp.children.append(('user_id', request.session['user_info']['id'],)) temp.children.append(('booking_date', booking_date,)) temp.children.append(('room_id', room_id,)) temp.children.append(('booking_time', time_id,)) remove_booking.add(temp, 'OR') if remove_booking: models.Booking.objects.filter(remove_booking).delete() except IntegrityError as e: ret['code'] = 1011 ret['msg'] = '會議室已被預定' except Exception as e: ret['code'] = 1012 ret['msg'] = '預定失敗:%s' % str(e) return JsonResponse(ret)
最后生成的頁面例子:
注:淡色為別人預定,不可操作;深色為自己預定,可退訂;咖色為選中,還未提交。

