会议室预定(可作为插件使用)
会议室预定(小项目)
该项目仍旧是用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)
最后生成的页面例子:
注:淡色为别人预定,不可操作;深色为自己预定,可退订;咖色为选中,还未提交。