会议室预订系统
一、目标及业务流程
期望效果:
业务流程:
-
用户注册
-
用户登录
-
预订会议室
-
退订会议室
-
选择日期;今日以及以后日期
二、表结构设计和生成
1、models.py(用户继承AbstractUser)
from django.db import models from django.contrib.auth.models import AbstractUser # Create your models here. class UserInfo(AbstractUser): tel = models.CharField(max_length=32,verbose_name="电话") avatar = models.FileField(upload_to="avatars/", default="avatars/timg.jpg", verbose_name="头像") class Room(models.Model): """会议室表""" caption = models.CharField(max_length=32,verbose_name="会议室名称") num = models.IntegerField(verbose_name="容纳人数") # 容纳人数 def __str__(self): return self.caption class Meta: verbose_name = "会议室信息" verbose_name_plural = verbose_name class Book(models.Model): """会议室预订""" user = models.ForeignKey(to="UserInfo",on_delete=models.CASCADE) room = models.ForeignKey(to="Room",on_delete=models.CASCADE) date = models.DateField() time_choice = ( (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"), (14, "21:00"), (15, "22:00"), (16, "23:00"), ) time_id = models.IntegerField(choices=time_choice) def __str__(self): return str(self.user)+"预定了"+str(self.room) class Meta: verbose_name = "预定信息" verbose_name_plural = verbose_name unique_together = ( ("room","date","time_id"), # 这三个字段联合唯一,防止重复预订 )
2、修改配置文件settings.py,覆盖默认的User模型
Django允许你通过修改setting.py文件中的 AUTH_USER_MODEL 设置覆盖默认的User模型,其值引用一个自定义的模型。
1
|
AUTH_USER_MODEL
=
"app01.UserInfo"
|
上面的值表示Django应用的名称(必须位于INSTALLLED_APPS中)和你想使用的User模型的名称。
注意:在创建任何迁移或者第一次运行 manager.py migrate 前设置 AUTH_USER_MODEL。
设置AUTH_USER_MODEL数据库结构有很大的影响。改变了一些会使用到的表格,并且会影响到一些外键和多对多关系的构造。在你有表格被创建后更改此设置是不被 makemigrations 支持的,并且会导致你需要手动修改数据库结构,从旧用户表中导出数据,可能重新应用一些迁移。
3、数据迁移及创建超级用户
1
2
|
$ python3 manage.py makemigrations
$ python3 manage.py migrate
|
三、系统登录login
urls.py:
from django.conf.urls import url from django.contrib import admin from django.views.static import serve from django.conf import settings from app01 import views urlpatterns = [ url(r'^admin/', admin.site.urls), # 用户登录 url(r'^login/',views.acc_login), # 展示预订信息 url(r'^index/',views.index), # 极验滑动验证码 获取验证码的url url(r'^pc-geetest/register', views.get_geetest), # media相关的路由设置 url(r'^media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}), # 处理预订请求 url(r'^book/',views.book), # 首页 url(r'^home/',views.home), # 注销 url(r'^logout/',views.acc_logout), # 用户注册 url(r'^reg/',views.reg), # 临时测试 url(r'^test/',views.test), # 修改密码 url(r'^change_password/',views.change_password), ]
login.html(使用了滑动验证)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用户登录</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <!-- 引入封装了failback的接口--initGeetest --> <script src="http://static.geetest.com/static/tools/gt.js"></script> </head> <body> <h3 class="text-center" style="color: orangered">欢迎登录会议室预订系统</h3> <br> <div class="container"> <div class="row"> <form class="form-horizontal col-md-6 col-md-offset-4" autocomplete="off"> {% csrf_token %} <div class="form-group"> <label for="username" class="col-lg-2 control-label">用户名</label> <div class="col-sm-6"> <input type="text" class="form-control" id="username" name="username" placeholder="用户名"> </div> </div> <div class="form-group"> <label for="pwd" class="col-lg-2 control-label">密码</label> <div class="col-sm-6"> <input type="password" class="form-control" id="pwd" name="pwd" placeholder="密码"> </div> </div> <div class="form-group"> <!-- 放置极验的滑动验证码 --> <div id="popup-captcha"></div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="button" id="login_btn" class="btn btn-info">登录</button> <span class="login-error has-error text-danger"></span> </div> </div> </form> </div> </div> <script src="/static/js/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> <script> var handlerPopup = function (captchaObj) { // 成功的回调 captchaObj.onSuccess(function () { var validate = captchaObj.getValidate(); // 1. 取到用户填写的用户名和密码 -> 取input框的值 var username = $("#username").val(); var password = $("#pwd").val(); $.ajax({ url: "/login/", // 进行二次验证 type: "post", dataType: "json", data: { username: username, pwd: password, csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(), geetest_challenge: validate.geetest_challenge, geetest_validate: validate.geetest_validate, geetest_seccode: validate.geetest_seccode }, success: function (data) { console.log(data); if (data.status) { // 有错误,在页面上提示 $(".login-error").text(data.msg); } else { // 登陆成功 location.href = data.msg; } } }); }); $("#login_btn").click(function () { captchaObj.show(); }); // 将验证码加到id为captcha的元素里 captchaObj.appendTo("#popup-captcha"); // 更多接口参考:http://www.geetest.com/install/sections/idx-client-sdk.html }; //当再次点击input输入框时,错误提示要消失 $("#username,#pwd").focus(function () { $(".login-error").text(""); }) // 验证开始需要向网站主后台获取id,challenge,success(是否启用failback) $.ajax({ url: "/pc-geetest/register?t=" + (new Date()).getTime(), // 加随机数防止缓存 type: "get", dataType: "json", success: function (data) { // 使用initGeetest接口 // 参数1:配置参数 // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件 initGeetest({ gt: data.gt, challenge: data.challenge, product: "popup", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效 offline: !data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注 // 更多配置参数请参见:http://www.geetest.com/install/sections/idx-client-sdk.html#config }, handlerPopup); } }) </script> </body> </html>
login视图函数
from django.shortcuts import render,redirect, HttpResponse from django.contrib.auth import authenticate, login, logout from django.http import JsonResponse from geetest import GeetestLib from django.contrib.auth.decorators import login_required from app01 import models from app01 import forms import json import datetime # 登录视图 def acc_login(request): if request.method == "POST": print(request.POST) res = {"status": 0, "msg": ""} username = request.POST.get("username") password = request.POST.get("pwd") # 获取极验 滑动验证码相关的参数 gt = GeetestLib(pc_geetest_id, pc_geetest_key) challenge = request.POST.get(gt.FN_CHALLENGE, '') validate = request.POST.get(gt.FN_VALIDATE, '') seccode = request.POST.get(gt.FN_SECCODE, '') status = request.session[gt.GT_STATUS_SESSION_KEY] user_id = request.session["user_id"] if status: result = gt.success_validate(challenge, validate, seccode, user_id) else: result = gt.failback_validate(challenge, validate, seccode) print("####################", result) if result: user = authenticate(username=username, password=password) if user: login(request, user) res["msg"] = "/index/" else: res["status"] =1 res["msg"] = "认证失败,请检查用户名及密码是否正确" else: res["status"] = 1 res["msg"] = "验证码错误" print("**************", res) return JsonResponse(res) return render(request, 'login.html') # 请在官网申请ID使用,示例ID不可使用 pc_geetest_id = "b46d1900d0a894591916ea94ea91bd2c" pc_geetest_key = "36fc3fe98530eea08dfc6ce76e3d24c4" # 处理极验 获取验证码的视图 def get_geetest(request): user_id = 'test' gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = gt.pre_process(user_id) request.session[gt.GT_STATUS_SESSION_KEY] = status request.session["user_id"] = user_id response_str = gt.get_response_str() return HttpResponse(response_str)
注意:auth模块的authenticate()方法,提供了用户认证,如果认证信息有效,会返回一个 User 对象;如果认证失败,则返回None。
四、index部分
1、引入admin组件(后台数据管理组件)并完成admin注册
admin.py
from django.contrib import admin from app01 import models from django.contrib.auth.admin import UserAdmin from django.utils.translation import gettext_lazy # Register your models here. # 配置会议室信息表 class RoomConfig(admin.ModelAdmin): list_display = ('caption','num') list_filter=('num',) search_fields = ('caption','num') # 配置预订信息表 class BookConfig(admin.ModelAdmin): list_display = ('user','room','date','time_id') list_filter = ('user','room','date','time_id') search_fields = ('user','room','date','time_id') # 配置用户管理表 class UserProfileAdmin(UserAdmin): list_display = ('username','last_login','is_superuser','is_staff','is_active','date_joined') list_filter = ('last_login', 'is_staff', 'date_joined', 'is_active') search_fields = ('username',) fieldsets = ( (None,{'fields':('username','password','first_name','last_name','email')}), (gettext_lazy('用户信息'),{'fields':('username','email','tel','avatar')}), (gettext_lazy('用户权限'), {'fields': ('is_superuser','is_staff','is_active', 'groups', 'user_permissions')}), (gettext_lazy('Important dates'), {'fields': ('last_login', 'date_joined')}), ) admin.site.register(models.Room,RoomConfig) admin.site.register(models.UserInfo,UserProfileAdmin) admin.site.register(models.Book,BookConfig)
注意:配置用户管理表至关重要,如果不配置,你会发现在登录admin后添加用户时密码是明文,并没有被加密。
如果你的用户扩展表没有扩展新的字段,可以直接admin.site.register(models.UserInfo,UserAdmin)。
2、登录admin添加数据
3、index视图函数数据处理和index.html模板渲染
index视图
@login_required(login_url="/login/") def index(request): date = datetime.datetime.now().date() # 如果没有指定日期,默认使用当天日期 book_date = request.GET.get("book_date",date) print('日期:', request.GET.get("book_date")) print("book_date",book_date) # 获取会议室时间段列表 time_choice = models.Book.time_choice print(time_choice) # 获取会议室列表 room_list = models.Room.objects.all() # 获取会议室预订信息 book_list = models.Book.objects.filter(date=book_date) htmls='' for room in room_list: htmls += '<tr><td>{}({})</td>'.format(room.caption,room.num) for time in time_choice: # 判断该单元格是否被预订 flag = False for book in book_list: if book.room.pk == room.pk and book.time_id == time[0]: # 单元格被预定 flag = True break if flag: # 判断当前登录人与预订会议室的人是否一致,一致使用info样式 if request.user.username == book.user.username: htmls += '<td class="info item" room_id={} time_id={}>{}</td>'.format(room.pk, time[0],book.user.username) else: htmls += '<td class="success item" room_id={} time_id={}>{}</td>'.format(room.pk, time[0], book.user.username) else: htmls += '<td class="item" room_id={} time_id={}></td>'.format(room.pk,time[0]) htmls += "</tr>" return render(request,'index.html',{"time_choice":time_choice,"htmls":htmls,})
index前端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/datetimepicker/bootstrap-datetimepicker.min.css"> <style> .td_active{ background-color: purple; } #my_div{ top: 215px!important; } </style> </head> <body> <div class="page-header"> <h1 class="text-center">欢迎来到会议室预订系统 <small class="text-info">{{ request.user.username }}</small></h1> </div> <div class="text-center"> <span>当前用户:<img src="/static/img/info.png" alt=""></span> <span>其他用户:<img src="/static/img/success.png" alt=""></span> </div> <br> <br> <p class="text-center"> <span><a href="/home/">返回首页</a></span> <span><a href="/logout/">注销</a></span> </p> <div class="calender pull-right"> <div class='input-group' style="width: 230px;"> <span class="text-warning">注意:当前日期高亮显示</span> <input type='text' autocomplete="off" class="form-control" id='datetimepicker11' placeholder="请选择日期"/> <span class="input-group-addon"> <span class="glyphicon glyphicon-calendar"> </span> </span> </div> </div> <br> <br> <table class="table table-bordered"> <thead> <tr> <th>会议室/时间</th> {% for row in time_choice %} <th>{{ row.1 }}</th> {% endfor %} </tr> </thead> <tbody> {{ htmls|safe }} </tbody> </table> <div >{% csrf_token %}</div> <div class="col-lg-offset-6" > <button class="btn btn-info book_btn">预订</button> </div> <script src="/static/js/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> <script src="/static/datetimepicker/bootstrap-datetimepicker.min.js"></script> <script src="/static/datetimepicker/bootstrap-datetimepicker.zh-CN.js"></script> <script> // 日期格式化方法 Date.prototype.yun = function (fmt) { //author:yun 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; }; TODAY_DATE=new Date().yun("yyyy-MM-dd");//获取当前日期 var POST_DATA={ "ADD":{}, "DEL":{}, }; function TdClick() { $(".item").click(function () { var room_id = $(this).attr("room_id"); var time_id = $(this).attr("time_id"); //取消预订 if($(this).hasClass("info")){ $(this).removeClass("info").empty(); if (POST_DATA.DEL[room_id]){ POST_DATA.DEL[room_id].push(time_id); } else {POST_DATA.DEL[room_id]=[time_id,];} } //取消临时预订 else if($(this).hasClass("td_active")){ $(this).removeClass("td_active"); //console.log(room_id,time_id) var index=$.inArray(time_id,POST_DATA.ADD[room_id]); POST_DATA.ADD[room_id].splice(index,1); //console.log(POST_DATA.ADD[room_id]); console.log(POST_DATA); } // 增加预订 else { $(this).addClass("td_active"); if (POST_DATA.ADD[room_id]){ POST_DATA.ADD[room_id].push(time_id); } else {POST_DATA.ADD[room_id]=[time_id,];} console.log(POST_DATA); } }); }; TdClick(); // 日期 if (location.search.slice(11)){ CHOOSE_DATE = location.search.slice(11) } else { CHOOSE_DATE = new Date().yun('yyyy-MM-dd'); console.log(CHOOSE_DATE); } // 通过ajax发送数据到后端 $(".book_btn").click(function () { $.ajax({ url:"/book/", type:"post", data:{ choose_date:CHOOSE_DATE, csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), post_data:JSON.stringify(POST_DATA), }, dataType:"json", success:function (data) { console.log(data); if(data.status==1){ alert("预订成功"); location.href=""; }else if (data.status==2){ alert("未修改信息"); location.href=""; } else { alert("已经被预定") location.href="" } }, }); }); // 日历插件 function book_query(e) { CHOOSE_DATE=e.date.yun("yyyy-MM-dd"); location.href="/index/?book_date="+CHOOSE_DATE; }; /** 判断输入框中输入的日期格式为yyyy-mm-dd和正确的日期 */ function isDate(data){ var filter = /((^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(10|12|0?[13578])([-\/\._])(3[01]|[12][0-9]|0?[1-9])$)|(^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(11|0?[469])([-\/\._])(30|[12][0-9]|0?[1-9])$)|(^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(0?2)([-\/\._])(2[0-8]|1[0-9]|0?[1-9])$)|(^([2468][048]00)([-\/\._])(0?2)([-\/\._])(29)$)|(^([3579][26]00)([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][0][48])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][0][48])([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][2468][048])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][2468][048])([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][13579][26])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][13579][26])([-\/\._])(0?2)([-\/\._])(29)$))/; if (filter.test(data)){ return true; }else { return false; } } $("#datetimepicker11").change(function () { var test = $(this).val(); if(isDate(test)){ if(test<TODAY_DATE){ alert("注意:日期不能小于当前日期!") } CHOOSE_DATE=test; location.href="/index/?book_date="+CHOOSE_DATE; }else { alert("日期格式错误!"); location.href=''; } }); $('#datetimepicker11').datetimepicker({ minView : 2, startView:2, language: "zh-CN", sideBySide: true, format: 'yyyy-mm-dd', startDate: TODAY_DATE, todayBtn:true, todayHighlight: 1,//当天日期高亮 enterLikeTab: false, bootcssVer:3, autoclose:true, }).on('changeDate',book_query).val(CHOOSE_DATE).css('font-weight','bold'); $(".datetimepicker.datetimepicker-dropdown-bottom-right.dropdown-menu").attr("id" ,"my_div"); </script> </body> </html>
注意:
(1)数据处理还是在后台更加方便,前台渲染后台传递来的标签字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<table
class
=
"table table-bordered table-striped"
>
<thead>
<tr>
<th>会议室时间<
/
th>
{
%
for
time_choice
in
time_choices
%
}
{
# 在元组中取第二个值 #}
<th>{{ time_choice.
1
}}<
/
th>
{
%
endfor
%
}
<
/
tr>
<
/
thead>
<tbody>
{
# 由于模板语法功能不够强大,因此数据处理还是放在后台,在这里渲染后台传递来的标签字符串 #}
{{ htmls|safe }}
<
/
tbody>
<
/
table>
|
由于模板语法功能不够强大,因此数据处理还是放在后台,在这里渲染后台传递来的标签字符串。
(2)视图函数字符串处理,运用format格式化函数
1
2
3
4
5
6
7
8
9
10
11
12
|
def
index(request):
# 拿到预定表中的时间段
time_choices
=
Book.time_choices
# 拿到所有的会议室
room_list
=
Room.objects.
all
()
# 构建标签
htmls
=
""
for
room
in
room_list:
# 第一列td完成后,还有其他td标签需要添加,因此此处没有闭合tr
htmls
+
=
"<tr><td>{}({})</td>"
.
format
(room.caption, room.num)
return
render(request,
"index.html"
,
locals
())
|
显示效果:
(3)循环会议室生成行,循环时段生成列,标签字符串拼接处理
def index(request): # 拿到预定表中的时间段 time_choices = Book.time_choices # 拿到所有的会议室 room_list = Room.objects.all() # 构建标签 htmls = "" for room in room_list: # 有多少会议室生成多少行, # 每行仅生成了第一列。还有其他td标签需要添加,因此此处没有闭合tr htmls += "<tr><td>{}({})</td>".format(room.caption, room.num) for time_choice in time_choices: # 有多少时段就生成多少列 # 一次循环就是一个td标签 htmls += "<td></td>" # 循环完成后闭合tr标签 htmls += "</tr>" return render(request, "index.html", locals())
比如会议室有3个,循环会议室生成三行,且拿到会议室名称和人数限制生成首列;再循环时段,这里有13个时段,因此生成13列,13个td标签依次添加进一个tr中,显示效果如下:
(4)给td标签添加room_id和time_id属性
for time_choice in time_choices: # 有多少时段就生成多少列 # 一次循环就是一个td标签 htmls += "<td room_id={} time_id={}></td>".format(room.pk, time_choice[0])
这样点击单元格可确定点击的是哪个会议室哪一个时段的单元格,效果如下所示:
(5)获取预约日期信息
import datetime
def index(request):
# 取当前日期
date = datetime.datetime.now().date()
print(date)
# 取预约日期,没有指定取当前日期
book_date = request.GET.get("book_date", date)
print(book_date)
index页面访问中,如果没有指定日期,默认显示的就是当前日的预定信息。
因此在循环生成表格时,可以循环确定单元格是否被预定,已经被预定的添加class=‘success’属性。
# 构建标签 htmls = "" for room in room_list: # 有多少会议室生成多少行, # 每行仅生成了第一列。还有其他td标签需要添加,因此此处没有闭合tr htmls += "<tr><td>{}({})</td>".format(room.caption, room.num) for time_choice in time_choices: # 有多少时段就生成多少列 flag = False # False代表没有预定,True代表已经预定 for book in book_list: # 循环确定单元格是否被预定 if book.room.pk == room.pk and book.time_id == time_choice[0]: # 符合条件说明当前时段会议室已经被预定 flag = True break if flag: # 已经被预定,添加class='success' htmls += "<td class='success' room_id={} time_id={}></td>".format(room.pk, time_choice[0]) else: htmls += "<td room_id={} time_id={}></td>".format(room.pk, time_choice[0]) # 循环完成后闭合tr标签 htmls += "</tr>"
(6)在预定单元格添加预定人姓名,并根据登录人判断显示单元格
if flag: # 已经被预定,添加class='active' if request.user.pk == book.user.pk: # 当前登录人查看自己的预约信息 htmls += "<td class='info item' room_id={} time_id={}>{}</td>".format(room.pk, time_choice[0], book.user.username) else: # 非当前登录人自己的预约信息 htmls += "<td class='success item' room_id={} time_id={}>{}</td>".format(room.pk, time_choice[0], book.user.username) else: htmls += "<td room_id={} time_id={}></td>".format(room.pk, time_choice[0])
显示效果如下:
五、前端部分数据处理(index.html)
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/datetimepicker/bootstrap-datetimepicker.min.css"> <style> .td_active{ background-color: purple; } #my_div{ top: 215px!important; } </style> </head> <body> <div class="page-header"> <h1 class="text-center">欢迎来到会议室预订系统 <small class="text-info">{{ request.user.username }}</small></h1> </div> <div class="text-center"> <span>当前用户:<img src="/static/img/info.png" alt=""></span> <span>其他用户:<img src="/static/img/success.png" alt=""></span> </div> <br> <br> <p class="text-center"> <span><a href="/home/">返回首页</a></span> <span><a href="/logout/">注销</a></span> </p> <div class="calender pull-right"> <div class='input-group' style="width: 230px;"> <span class="text-warning">注意:当前日期高亮显示</span> <input type='text' autocomplete="off" class="form-control" id='datetimepicker11' placeholder="请选择日期"/> <span class="input-group-addon"> <span class="glyphicon glyphicon-calendar"> </span> </span> </div> </div> <br> <br> <table class="table table-bordered"> <thead> <tr> <th>会议室/时间</th> {% for row in time_choice %} <th>{{ row.1 }}</th> {% endfor %} </tr> </thead> <tbody> <!--由于模板语法功能不够强大,因此数据处理还是放在后台,在这里渲染后台传递来的标签字符串--> {{ htmls|safe }} </tbody> </table> <div >{% csrf_token %}</div> <div class="col-lg-offset-6" > <button class="btn btn-info book_btn">预订</button> </div> <script src="/static/js/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> <script src="/static/datetimepicker/bootstrap-datetimepicker.min.js"></script> <script src="/static/datetimepicker/bootstrap-datetimepicker.zh-CN.js"></script> <script> // 日期格式化方法 Date.prototype.yun = function (fmt) { //author:yun 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; }; TODAY_DATE=new Date().yun("yyyy-MM-dd");//获取当前日期 var POST_DATA={ "ADD":{}, "DEL":{}, }; function TdClick() { $(".item").click(function () { var room_id = $(this).attr("room_id"); var time_id = $(this).attr("time_id"); //取消预订 if($(this).hasClass("info")){ // 如果点击的标签具有info类,直接删除info类并清空内容 $(this).removeClass("info").empty(); if (POST_DATA.DEL[room_id]){ // 在数据中已经存有会议室信息,将新单元格time_id添加进数组 POST_DATA.DEL[room_id].push(time_id); } else // 在数据中没有存过对应会议室记录,直接将time_id对其赋值创建一个字典 {POST_DATA.DEL[room_id]=[time_id,];} } //取消临时预订 else if($(this).hasClass("td_active")){ $(this).removeClass("td_active"); //点击删除临时预订的数据 var index=$.inArray(time_id,POST_DATA.ADD[room_id]); POST_DATA.ADD[room_id].splice(index,1); } // 增加预订 else { $(this).addClass("td_active"); if (POST_DATA.ADD[room_id]){ // 在数据中已经存有会议室信息,将新单元格time_id添加进数组 POST_DATA.ADD[room_id].push(time_id); } else // 在数据中没有存过对应会议室记录,直接将time_id对其赋值创建一个字典 {POST_DATA.ADD[room_id]=[time_id,];} } }); }; TdClick(); // 日期 if (location.search.slice(11)){ CHOOSE_DATE = location.search.slice(11) } else { CHOOSE_DATE = new Date().yun('yyyy-MM-dd'); } // 通过ajax发送数据到后端 $(".book_btn").click(function () { $.ajax({ url:"/book/", type:"post", data:{ choose_date:CHOOSE_DATE, csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), post_data:JSON.stringify(POST_DATA), }, dataType:"json", success:function (data) { console.log(data); if(data.status==1){ alert("预订成功"); location.href=""; }else if (data.status==2){ alert("未修改信息"); location.href=""; } else { alert("已经被预定") location.href="" } }, }); }); // 日历插件 function book_query(e) { CHOOSE_DATE=e.date.yun("yyyy-MM-dd"); location.href="/index/?book_date="+CHOOSE_DATE; }; /** 判断输入框中输入的日期格式为yyyy-mm-dd和正确的日期 */ function isDate(data){ var filter = /((^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(10|12|0?[13578])([-\/\._])(3[01]|[12][0-9]|0?[1-9])$)|(^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(11|0?[469])([-\/\._])(30|[12][0-9]|0?[1-9])$)|(^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(0?2)([-\/\._])(2[0-8]|1[0-9]|0?[1-9])$)|(^([2468][048]00)([-\/\._])(0?2)([-\/\._])(29)$)|(^([3579][26]00)([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][0][48])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][0][48])([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][2468][048])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][2468][048])([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][13579][26])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][13579][26])([-\/\._])(0?2)([-\/\._])(29)$))/; if (filter.test(data)){ return true; }else { return false; } } $("#datetimepicker11").change(function () { var test = $(this).val(); if(isDate(test)){ if(test<TODAY_DATE){ alert("注意:日期不能小于当前日期!") } CHOOSE_DATE=test; location.href="/index/?book_date="+CHOOSE_DATE; }else { alert("日期格式错误!"); location.href=''; } }); $('#datetimepicker11').datetimepicker({ minView : 2, startView:2, language: "zh-CN", sideBySide: true, format: 'yyyy-mm-dd', startDate: TODAY_DATE, todayBtn:true, todayHighlight: 1,//当天日期高亮 enterLikeTab: false, bootcssVer:3, autoclose:true, }).on('changeDate',book_query).val(CHOOSE_DATE).css('font-weight','bold'); $(".datetimepicker.datetimepicker-dropdown-bottom-right.dropdown-menu").attr("id" ,"my_div"); </script> </body> </html>
1、点击事件预定和取消——组织数据
var POST_DATA={
"ADD":{},
"DEL":{},
};
function TdClick() {
$(".item").click(function () {
var room_id = $(this).attr("room_id");
var time_id = $(this).attr("time_id");
//取消预订
if($(this).hasClass("info")){
// 如果点击的标签具有info类,直接删除info类并清空内容
$(this).removeClass("info").empty();
if (POST_DATA.DEL[room_id]){
// 在数据中已经存有会议室信息,将新单元格time_id添加进数组
POST_DATA.DEL[room_id].push(time_id);
}
else
// 在数据中没有存过对应会议室记录,直接将time_id对其赋值创建一个字典
{POST_DATA.DEL[room_id]=[time_id,];}
}
//取消临时预订
else if($(this).hasClass("td_active")){
$(this).removeClass("td_active");
//点击删除临时预订的数据
var index=$.inArray(time_id,POST_DATA.ADD[room_id]);
POST_DATA.ADD[room_id].splice(index,1);
}
// 增加预订
else {
$(this).addClass("td_active");
if (POST_DATA.ADD[room_id]){
// 在数据中已经存有会议室信息,将新单元格time_id添加进数组
POST_DATA.ADD[room_id].push(time_id);
}
else
// 在数据中没有存过对应会议室记录,直接将time_id对其赋值创建一个字典
{POST_DATA.ADD[room_id]=[time_id,];}
}
});
};
TdClick();
注意:
(1)取消预定事件
<script> // 为td绑定单击事件 function BindTd() { $('.item').click(function () { // alert($(this).attr("room_id")); // 点击显示会议室id // 取消预定 if ($(this).hasClass("info")){ // 如果点击的标签具有active类,直接删除active类并清空内容 $(this).removeClass("info").empty(); } else if ($(this).hasClass("td_active")) { $(this).removeClass("td_active"); } else { // 空白局域点击 $(this).addClass("td_active"); } }) } BindTd(); </script>
在这次只处理了具有info类和td_active类的情况,但没有处理success类的情况,因为这种需要判断的情况,一定要交给后端,否则就是前端一套后端一套,点击保存按钮发送的js,客户可以伪装一个data发送给服务器,如果不做联合唯一,完全交给前端会造成很严重的安全问题。
(2)数据组织和添加预定
创建如下所示用js字面量方式创建对象
POST_DATA,有两个属性(对象)ADD和DEL,这两个对象的值以room_id为键,以time_id为值:
<script> // room_id 为键,time_id 为值 {1:[4,5],2:[4,] } {3:[9,10]} var POST_DATA = { "ADD":{}, "DEL":{} }; </script>
在空白单元格点击,获取添加数据到POST_DATA中,以完成预定工作:
function TdClick() {
$(".item").click(function () {
var room_id = $(this).attr("room_id");
var time_id = $(this).attr("time_id");
//取消预订
if($(this).hasClass("info")){
// 如果点击的标签具有info类,直接删除info类并清空内容
$(this).removeClass("info").empty();
if (POST_DATA.DEL[room_id]){
// 在数据中已经存有会议室信息,将新单元格time_id添加进数组
POST_DATA.DEL[room_id].push(time_id);
}
else
// 在数据中没有存过对应会议室记录,直接将time_id对其赋值创建一个字典
{POST_DATA.DEL[room_id]=[time_id,];}
}
//取消临时预订
else if($(this).hasClass("td_active")){
$(this).removeClass("td_active");
//点击删除临时预订的数据
var index=$.inArray(time_id,POST_DATA.ADD[room_id]);
POST_DATA.ADD[room_id].splice(index,1);
}
// 增加预订
else {
$(this).addClass("td_active");
if (POST_DATA.ADD[room_id]){
// 在数据中已经存有会议室信息,将新单元格time_id添加进数组
POST_DATA.ADD[room_id].push(time_id);
}
else
// 在数据中没有存过对应会议室记录,直接将time_id对其赋值创建一个字典
{POST_DATA.ADD[room_id]=[time_id,];}
console.log(POST_DATA.ADD);
}
});
};
TdClick();
(3)临时预定取消数据处理
//取消临时预订
else if($(this).hasClass("td_active")){
$(this).removeClass("td_active");
//点击删除临时预订的数据
var index=$.inArray(time_id,POST_DATA.ADD[room_id]);
POST_DATA.ADD[room_id].splice(index,1);
}
(4)js数组操作常用方法
1 // shift:删除原数组第一项,并返回删除元素的值;如果数组为空则返回undefined 2 var a = [1,2,3,4,5]; 3 var b = a.shift(); //a:[2,3,4,5] b:1 4 5 // pop:删除原数组最后一项,并返回删除元素的值;如果数组为空则返回undefined 6 var a = [1,2,3,4,5]; 7 var b = a.pop(); //a:[1,2,3,4] b:5 8 9 // push:将参数添加到原数组末尾,并返回数组的长度 10 var a = [1,2,3,4,5]; 11 var b = a.push(6,7); //a:[1,2,3,4,5,6,7] b:7 12 13 // concat:返回一个新数组,是将参数添加到原数组中构成的 14 var a = [1,2,3,4,5]; 15 var b = a.concat(6,7); //a:[1,2,3,4,5] b:[1,2,3,4,5,6,7] 16 17 // splice(start,deleteCount,val1,val2,...):从start位置开始删除deleteCount项,并从该位置起插入val1,val2,... 18 var a = [1,2,3,4,5]; 19 var b = a.splice(2,2,7,8,9); //a:[1,2,7,8,9,5] b:[3,4] 20 var b = a.splice(0,1); //同shift 21 a.splice(0,0,-2,-1); var b = a.length; //同unshift 22 var b = a.splice(a.length-1,1); //同pop 23 a.splice(a.length,0,6,7); var b = a.length; //同push 24 25 // reverse:将数组反序 26 // sort(orderfunction):按指定的参数对数组进行排序 27 28 // slice(start,end):返回从原数组中指定开始下标到结束下标之间的项组成的新数组 29 var a = [1,2,3,4,5]; 30 var b = a.slice(2,5); //a:[1,2,3,4,5] b:[3,4,5] 31 32 // join(separator):将数组的元素组起一个字符串,以separator为分隔符,省略的话则用默认用逗号为分隔符 33 var a = [1,2,3,4,5]; 34 var b = a.join("|"); //a:[1,2,3,4,5] b:"1|2|3|4|5"
网络编程本质是浏览器和服务器之间发送字符串,以POST请求为例
POST: 浏览器-------------------->server "请求首行\r\nContent-Type:url_encode\r\n\r\na=1&b=2" "请求首行\r\nContent-Type:application/json\r\n\r\n'{"a":1,"b":2}'" 在django的wsgi的request中: request.body:元数据'{"a":1,"b":2}' if 请求头中的Content-Type==url_encode: request.POST=解码a=1&b=2
2、日历插件(datetimepicker)官方文档:http://eonasdan.github.io/bootstrap-datetimepicker/
日历html
<div class="calender pull-right"> <div class='input-group' style="width: 230px;"> <span class="text-warning">注意:当前日期高亮显示</span> <input type='text' autocomplete="off" class="form-control" id='datetimepicker11' placeholder="请选择日期"/> <span class="input-group-addon"> <span class="glyphicon glyphicon-calendar"> </span> </span> </div> </div>
日历js代码
// 日期格式化方法 Date.prototype.yun = function (fmt) { //author:yun 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; }; TODAY_DATE=new Date().yun("yyyy-MM-dd");//获取当前日期 // 日期 if (location.search.slice(11)){ CHOOSE_DATE = location.search.slice(11) } else { CHOOSE_DATE = new Date().yun('yyyy-MM-dd'); } // 日历插件 function book_query(e) { CHOOSE_DATE=e.date.yun("yyyy-MM-dd"); location.href="/index/?book_date="+CHOOSE_DATE; }; /** 判断输入框中输入的日期格式为yyyy-mm-dd和正确的日期 */ function isDate(data){ var filter = /((^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(10|12|0?[13578])([-\/\._])(3[01]|[12][0-9]|0?[1-9])$)|(^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(11|0?[469])([-\/\._])(30|[12][0-9]|0?[1-9])$)|(^((1[8-9]\d{2})|([2-9]\d{3}))([-\/\._])(0?2)([-\/\._])(2[0-8]|1[0-9]|0?[1-9])$)|(^([2468][048]00)([-\/\._])(0?2)([-\/\._])(29)$)|(^([3579][26]00)([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][0][48])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][0][48])([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][2468][048])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][2468][048])([-\/\._])(0?2)([-\/\._])(29)$)|(^([1][89][13579][26])([-\/\._])(0?2)([-\/\._])(29)$)|(^([2-9][0-9][13579][26])([-\/\._])(0?2)([-\/\._])(29)$))/; if (filter.test(data)){ return true; }else { return false; } } $("#datetimepicker11").change(function () { var test = $(this).val(); if(isDate(test)){ if(test<TODAY_DATE){ alert("注意:日期不能小于当前日期!") } CHOOSE_DATE=test; location.href="/index/?book_date="+CHOOSE_DATE; }else { alert("日期格式错误!"); location.href=''; } }); //初始化日历 $('#datetimepicker11').datetimepicker({ minView : 2, startView:2, language: "zh-CN", sideBySide: true, format: 'yyyy-mm-dd', startDate: TODAY_DATE, todayBtn:true, todayHighlight: 1,//当天日期高亮 enterLikeTab: false, bootcssVer:3, autoclose:true, }).on('changeDate',book_query).val(CHOOSE_DATE).css('font-weight','bold'); $(".datetimepicker.datetimepicker-dropdown-bottom-right.dropdown-menu").attr("id" ,"my_div");
3、发送AJAX
// 通过ajax发送数据到后端
$(".book_btn").click(function () {
$.ajax({
url:"/book/",
type:"post",
data:{
choose_date:CHOOSE_DATE,
csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(),
post_data:JSON.stringify(POST_DATA),
},
dataType:"json",
success:function (data) {
console.log(data);
if(data.status==1){
alert("预订成功");
location.href="";
}else if (data.status==2){
alert("未修改信息");
location.href="";
}
else {
alert("已经被预定")
location.href=""
}
},
});
});
六、视图处理图书预定和取消
def book(request):
if request.method == "POST":
choose_date = request.POST.get("choose_date")
print("choose_date:", choose_date)
# 获取会议室时间段列表
time_choice = models.Book.time_choice
try:
# 向数据库修改会议室预订记录
post_data = json.loads(request.POST.get("post_data"))
if not post_data["ADD"] and not post_data["DEL"]:
res = {"status":2, "msg":""}
return HttpResponse(json.dumps(res))
user = request.user
print(type(post_data), post_data)
# 添加新的预订信息
book_list = []
for room_id, time_id_list in post_data["ADD"].items():
for time_id in time_id_list:
book_obj = models.Book(user=user, room_id=room_id, time_id=time_id, date=choose_date)
book_list.append(book_obj)
models.Book.objects.bulk_create(book_list)
# 删除旧的预订信息
from django.db.models import Q
remove_book = Q()
for room_id,time_id_list in post_data["DEL"].items():
temp = Q()
for time_id in time_id_list:
temp.children.append(("room_id", room_id))
temp.children.append(("time_id", time_id))
temp.children.append(("user_id", request.user.pk))
temp.children.append(("date", choose_date))
remove_book.add(temp, "OR")
if remove_book:
models.Book.objects.filter(remove_book).delete()
for time in post_data["DEL"][room_id]:
models.Book.objects.filter(user=user, room_id=room_id, time_id=time, date=choose_date).delete()
res = {"status": 1, "msg": ''}
except Exception as e:
res = {"status": 0, "msg": str(e)}
return HttpResponse(json.dumps(res))
七、用户注册
注册前端页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>欢迎注册</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <style> html, body { width: 100%; height: 100%; } #avatar-img { width: 80px; height: 80px; } .mui-content { background: url("/static/img/reg_bak.jpg") bottom center no-repeat #efeff4; background-size: 100% 100%; width: 100%; height: 100%; } </style> </head> <body> <div class="container mui-content"> <h3 class="text-center" style="color: orangered">欢迎注册会议室预订系统</h3> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form novalidate action="/reg/" method="post" autocomplete="off" class="form-horizontal reg-form" enctype="multipart/form-data"> {% csrf_token %} <!--头像--> <div class="form-group text-center" style="margin-top: 80px"> <label class="col-sm-2 control-label">头像</label> <div class="col-sm-8"> <label for="id_avatar"><img id="avatar-img" src="/static/img/timg.jpg" alt=""></label> <input accept="image/*" type="file" name="avatar" id="id_avatar" style="display: none"> <span class="help-block"></span> </div> </div> <!--用户名--> <div class="form-group"> <label for="{{ form_obj.username.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.username.label }}</label> <div class="col-sm-8"> {{ form_obj.username }} <span class="help-block">{{ form_obj.username.errors.0 }}</span> </div> </div> <!--邮箱--> <div class="form-group"> <label for="{{ form_obj.email.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.email.label }}</label> <div class="col-sm-8"> {{ form_obj.email }} <span class="help-block">{{ form_obj.email.errors.0 }}</span> </div> </div> <!--密码--> <div class="form-group"> <label for="{{ form_obj.password.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.password.label }}</label> <div class="col-sm-8"> {{ form_obj.password }} <span class="help-block">{{ form_obj.password.errors.0 }}</span> </div> </div> <!--确认密码--> <div class="form-group"> <label for="{{ form_obj.re_password.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.re_password.label }}</label> <div class="col-sm-8"> {{ form_obj.re_password }} <span class="help-block">{{ form_obj.re_password.errors.0 }}</span> </div> </div> <!--注册--> <div class="form-group"> <div class="col-sm-offset-4 col-sm-12"> <button type="button" class="btn btn-success" id="reg-submit">注册</button> <input type="reset" class="btn btn-danger" value="重置"> <a class="panel-warning" href="/home/">返回首页</a> </div> </div> </form> </div> </div> </div> <script src="/static/js/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> <script> // 找到头像的input标签绑定change事件 $("#id_avatar").change(function () { // 1. 创建一个读取文件的对象 var fileReader = new FileReader(); // 取到当前选中的头像文件 // console.log(this.files[0]); // 读取你选中的那个文件 fileReader.readAsDataURL(this.files[0]); // 读取文件是需要时间的 fileReader.onload = function () { // 2. 等上一步读完文件之后才 把图片加载到img标签中 $("#avatar-img").attr("src", fileReader.result); }; }); // AJAX提交注册的数据 $("#reg-submit").click(function () { // 取到用户填写的注册数据,向后端发送AJAX请求 var formData = new FormData(); formData.append("username", $("#id_username").val()); formData.append("password", $("#id_password").val()); formData.append("re_password", $("#id_re_password").val()); formData.append("email", $("#id_email").val()); formData.append("avatar", $("#id_avatar")[0].files[0]); formData.append("csrfmiddlewaretoken", $("[name='csrfmiddlewaretoken']").val()); $.ajax({ url: "/reg/", type: "post", processData: false, //告诉Jquery不要处理我的数据 contentType: false, //告诉jQuery不要设置content类型 data: formData, success:function (data) { if (data.status){ // 有错误就展示错误 // console.log(data.msg); // 将报错信息填写到页面上 $.each(data.msg, function (k,v) { // console.log("id_"+k, v[0]); // console.log($("#id_"+k)); $("#id_"+k).next("span").text(v[0]).parent().parent().addClass("has-error"); }) }else { // 没有错误就跳转到指定页面 location.href = data.msg; } } }) }); // 将所有的input框绑定获取焦点的事件,将所有的错误信息清空 $("form input").focus(function () { $(this).next().text("").parent().parent().removeClass("has-error"); }); //给username的input输入框,绑定失去焦点事件,失去焦点后检测用户名是否存在 $("#id_username").blur(function () { // 取到用户填写的值 var username = $(this).val(); //发ajax请求 $.ajax({ url:"/check_username_exist/", type:"get", data:{username:username,}, success:function (data) { if(data.status){ //有错误,用户名已被注册 $("#id_username").next().text(data.msg).parent().parent().addClass("has-error"); } } }) }) </script> </body> </html>
注册后端页面
def reg(request):
if request.method == "POST":
ret = {"status": 0, "msg": ""}
form_obj = forms.RegForm(request.POST)
print('request.POST'.center(80, '#'))
print(request.POST)
print('request.POST'.center(80, '#'))
avatar_img = request.FILES.get("avatar")
print(avatar_img)
# 帮我做校验
if form_obj.is_valid():
# 校验通过,去数据库创建一个新的用户
form_obj.cleaned_data.pop("re_password")
print(form_obj.cleaned_data)
try:
models.UserInfo.objects.create_user(**form_obj.cleaned_data,avatar=avatar_img)
except Exception as e:
print(e)
ret["msg"] = "/login/"
return JsonResponse(ret)
else:
print(form_obj.errors)
ret["status"] = 1
ret["msg"] = form_obj.errors
print(ret)
print("=" * 120)
return JsonResponse(ret)
# 生成一个form对象
form_obj = forms.RegForm()
print(form_obj.fields)
return render(request,'reg.html',{"form_obj": form_obj})
八、项目源码
https://github.com/Yun-Wangjun/BookSystem