会议室预定(小项目)
该项目仍旧是用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})
注销、登录的views
上面用到了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}))
LoginForm
{% 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>
login.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
装饰器auth
登录功能较为简单,不做详述,接下来我们做首页
我们的预定功能就在首页中,所以首页是重中之重。
难点:index.html中的js:tbody的生成、datetimepicker插件的使用、前后端发送的时间格式的转换、后端录入数据库的操作
url(r'^index/$', views.index),
url(r'^booking/$', views.booking),
#views.py中:
@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})
#views.py中:
import jsonimport datetimefrom django.shortcuts import render, HttpResponse, redirectfrom django.http import JsonResponsefrom meet import modelsfrom meet.form import *from django.db.models import Qfrom 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>
index.html(重点在js代码!!!)
#装饰器
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)
预定功能,重点!
最后生成的页面例子:
注:淡色为别人预定,不可操作;深色为自己预定,可退订;咖色为选中,还未提交。