会议室预定(可作为插件使用)

时间:2022-04-08 02:52:48

会议室预定(小项目)

该项目仍旧是用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中:
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>&nbsp;&nbsp;&nbsp;&nbsp;<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)
预定功能,重点!

 

 最后生成的页面例子:

注:淡色为别人预定,不可操作;深色为自己预定,可退订;咖色为选中,还未提交。

 会议室预定(可作为插件使用)