XY的小伙伴你好_TAPD(2022.7版本)测试日报发送

# --*--coding:utf-8--*--
# updateDate = 2022/08/09
# 已离职,图表联系刘超、李一鸣调整

import copy
import json
import webbrowser  # 打开网页
import random
from faker import Faker
import gevent
from gevent import monkey

monkey.patch_all()
import requests
from requests import exceptions
import re
import sqlite3
import datetime
import math
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.pyplot import MultipleLocator
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
import os
from chinese_calendar import is_workday, is_holiday, get_dates, get_workdays, find_workday
import configparser
import PySimpleGUI as sg
from email.utils import parseaddr
from email.utils import formataddr
from email.header import Header
import time
from PIL import ImageGrab
import unicodedata
loglevel = "info"
plt.rcParams.update({'figure.max_open_warning': 0})

month_names = ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月", ]


# 界面框架
def tool_ui(data):
    """
    工具所有的UI布局部分
    :param data: 传入从配置文件tapdconf.ini读取的数据
    :return:布局layout
    """
    """顶部条件输入区域"""
    display_bool = True
    layout_factor = [
        # 第一行
        [
            sg.Button(key='url_method', button_text="*    TAPD_Cookie:", size=15, button_color='#64778D',
                      border_width=0, tooltip='点击可切换数据请求方式'),
            sg.Input(key='cookie', size=33, default_text=data['cookie']),
            sg.Button("烎", tooltip='更新项目', size=3, mouseover_colors='#7c8577', use_ttk_buttons=True),
            sg.Text('* 项目:', size=9, justification='right'),
            sg.InputCombo(key='id', size=31, default_value=data['id'], values=data['list_id'].split('_toolflag_'),
                          enable_events=True),
            sg.Text('* 测试起止时间:', size=15, justification='right'),
            sg.Input(key='start_time', size=10, default_text=data['start_time'], tooltip='2021-01-01'),
            sg.Text('-', size=1),
            sg.Input(key='end_time', size=10, default_text=today, tooltip='2021-12-31'),
            sg.CalendarButton('DATE', font=('MS Sans Serif', 1, 'bold'), button_color=('#fffffb', '#11264f'),
                              format='%Y-%m-%d',
                              key='_CALENDAR_', target='start_time', tooltip='选择测试开始时间', month_names=month_names,
                              enable_events=True)
        ],
        # 第二行
        [
            sg.Text('迭代版本:', size=15, justification='right'),
            sg.InputCombo(key='iteration_id', size=31, default_value=data['iteration_id'], tooltip='根据迭代版本筛选缺陷',
                          values=data['list_iteration'].split('_toolflag_'), enable_events=True),
            sg.Text('发布计划:', size=15, justification='right'),
            sg.InputCombo(key='release_id', size=31, default_value=data['release_id'],
                          values=data['list_release_id'].split('_toolflag_'),
                          tooltip='项目发布计划id', enable_events=True),
            # sg.Input(key='release_id', size=33, default_text=data['release_id'], tooltip='根据缺陷标题内容筛选'),
            sg.Text('操作系统:', size=15, justification='right'),
            sg.InputCombo(key='os1', size=31, default_value=data['os1'], values=data['list_os'].split('_toolflag_'),
                          enable_events=True, tooltip='根据缺陷所选操作系统筛选,支持手输')
        ],
        # 第三行
        [
            sg.Text('测试阶段:', size=15, justification='right'),
            sg.InputCombo(key='testphase', size=31, default_value=data['testphase'], tooltip='根据缺陷所选测试阶段筛选缺陷,支持手输',
                          values=data['list_testphase'].split('_toolflag_'), enable_events=True),
            sg.Text('是否包节假日:', size=15, justification='right'),
            sg.Radio("是", "RADIO1", key='b2', default=(data['b2'] == 'True')),
            sg.Radio("否", "RADIO1", key='b1', default=(data['b1'] == 'True'), size=20),
            sg.Text("获取数据URL:", size=15, justification='right', visible=False),  # 留一个隐藏的占位
            sg.Input(key='url_tapd', size=141, default_text=data['url_tapd'], visible=False),
            sg.Checkbox('邮件定时(min):', key='timer', default=False),
            # sg.Spin([i for i in range(1, 110)], initial_value=1, size=31)
            sg.Slider(key='timer_slider', range=(1, 1440), default_value=120, size=(10, 10), expand_x=True,
                      orientation='horizontal', font=('Helvetica', 10))
        ],
        # 第四行
        [
            sg.Text(key='text1', text='* 邮件服务器地址:', size=15, justification='right', visible=display_bool),
            sg.Input(key='smtp_server', size=33, default_text=data['smtp_server'], tooltip='smtp.exmail.qq.com',
                     visible=display_bool),
            sg.Text(key='text2', text='* 邮件服务器账号:', size=15, justification='right', visible=display_bool),
            sg.Input(key='email_user', size=33, default_text=data['email_user'], tooltip='xxxx@ztccloud.com.cn',
                     visible=display_bool),
            sg.Text(key='text3', text='* 邮件服务器密码:', size=15, justification='right', visible=display_bool),
            sg.Input(key='email_passwd', size=33, password_char='*', default_text=data['email_passwd'],
                     visible=display_bool)
        ],
        # 第五行
        [
            sg.Text('邮件主题:', size=15, justification='right'),
            sg.Input(key='mail_title', size=33, default_text=data['mail_title']),
            sg.Text(key='text4', text='邮件收件人:', size=15, justification='right', visible=display_bool),
            sg.Input(key='addressee', size=33, tooltip='多个收件人用逗号隔开,邮件将直接发送至收件人,请慎重', default_text=data['addressee'],
                     visible=display_bool),
            sg.Text(key='text5', text='邮件抄送人:', size=15, justification='right', visible=display_bool),
            sg.Input(key='mail_cc', size=33, tooltip='抄送多人用逗号隔开', default_text=data['mail_cc'], visible=display_bool)
        ],
        # 按钮行
        [
            sg.Button("源码", size=10, mouseover_colors='#1d953f', use_ttk_buttons=True, right_click_menu=
            ['&Right', ['你的队友:', '【镇山的虎】', '【远见的鹰】', '【忠诚的狗】', '【善战的狼】',
                        '而你:', '【害群的马】', '【盛饭的桶】', '【墙头的草】', ['【卸载工具】'], '【搅屎的棍】']]),
            sg.Text('  ', size=56, justification='left'),  # 空白区域
            # sg.Button(key='collapse_control', button_text="︾", size=1, button_color='#64778D', border_width=0,
            #           visible=True),  # 展开符号  #
            sg.Text('  ', size=1, justification='left'),  # 空白区域
            sg.Text('  ', size=31, justification='left'),  # 空白区域
            sg.Button("执行", bind_return_key=True, size=10, mouseover_colors='#1d953f', use_ttk_buttons=True),
            sg.Button("暂存", size=10, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='暂存界面输入内容'),
            sg.Button("清空", size=10, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='清空执行日志')
        ]
    ]
    """邮件正文组成部分输入区域"""
    layout_mail_text = [
        [
            sg.Text('邮件开篇:', size=15, justification='left')
        ],
        [
            sg.Multiline(key='daily_text', size=(75, 6), default_text=data['daily_text'])
        ],
        [
            sg.Text('里程碑总体进度:', size=13, justification='left'),
            sg.Input(key='milestone_address', size=47, default_text=data['milestone_address'],
                     tooltip='里程碑在线文档地址,用于展示'),
            sg.Button("贴图", size=4, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='将剪切板的截图贴到邮件里程碑位置'),
            sg.Button("预览", size=4, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='查看里程碑截图')
        ],
        [
            sg.Multiline(key='milestone_excel', size=(75, 4),
                         tooltip='可粘贴里程碑在线文档内容,请直接粘贴excel\ntip:\n    单元格合并格式只会保留第一列\n    单元格里边存在换行也会有异常..',
                         default_text=data['milestone_excel'].replace("_toolflag_", "	"))
        ],
        [
            sg.Text('今日测试执行清况:', size=15, justification='left'),
            sg.Input(key='manpower_today', size=3, default_text=data['manpower_today'], justification='right',
                     background_color='#009ad6'),
            sg.Text('人天', size=4, justification='left')
        ],
        [
            sg.Multiline(key='test_situation', size=(75, 4), default_text=data['test_situation'])
        ],
        [
            sg.Text('明日计划:', auto_size_text=True, justification='left')
        ],
        [
            sg.Multiline(key='tomorrow_plans', size=(75, 4), default_text=data['tomorrow_plans'])
        ]
    ]
    """工具执行结果展示控件"""
    layout_result = [
        [
            sg.Output(key='debug_result', size=(75, 26), echo_stdout_stderr=True)
        ]
    ]
    """总体布局容器"""
    layout = [
        [
            sg.Frame(key='frame1', title='', layout=layout_factor)
        ],
        [
            sg.Frame(' 邮件内容:', layout_mail_text), sg.Frame(' 执行日志:', layout_result)
        ]
    ]
    return layout

# 自定义url方式获取tapd数据# 界面框架
def tool_ui2(data):
    layout = [
        # 第一行
        [
            sg.Button(key='url_method', button_text="*    TAPD_Cookie:", size=15, button_color='#64778D',
                      border_width=0),
            sg.Input(key='cookie', size=33, default_text=data['cookie']),
            sg.Text('* 测试起止时间:', size=15, justification='right'),
            sg.Input(key='start_time', size=10, default_text=data['start_time'], tooltip='2021-01-01'),
            sg.Text('-', size=1),
            sg.Input(key='end_time', size=10, default_text=today, tooltip='2021-12-31'),
            sg.CalendarButton('DATE', font=('MS Sans Serif', 1, 'bold'), button_color=('#fffffb', '#11264f'),
                              format='%Y-%m-%d',
                              key='_CALENDAR_', target='start_time', tooltip='选择测试开始时间', month_names=month_names,
                              enable_events=True),
            sg.Text('是否包含节假日:', size=15, justification='right'),
            sg.Radio("是", "RADIO1", key='b2', default=(data['b2'] == 'True')),
            sg.Radio("否", "RADIO1", key='b1', default=(data['b1'] == 'True'))
        ],
        # 第二行
        [
            sg.Text("Request Payload:", size=15, justification='right'),  # 空白区域
            sg.Input(key='url_tapd', size=141, default_text=data['url_tapd'],
                     tooltip='请填写【TAPD-缺陷-系统视图-所有的】这个筛选视图添加过滤条件后调的bugs_list接口的请求体;'
                             '\n请选择原json格式粘贴,GOOGLE浏览器F12选择[view source];'
                             '\n建议筛选中添加"创建时间"条件,起止时间与界面的"测试起止时间"保持一致,有利于数据一致性'),
            sg.Checkbox('邮件定时(min):', key='timer', default=False, visible=False),
            sg.Slider(key='timer_slider', range=(1, 1440), default_value=0, size=(10, 10), expand_x=True,
                      orientation='horizontal', font=('Helvetica', 10), visible=False)
        ],
        # 第三行
        [
            sg.Text(key='text1', text='* 邮件服务器地址:', size=15, justification='right'),
            sg.Input(key='smtp_server', size=33, default_text=data['smtp_server'], tooltip='smtp.exmail.qq.com'),
            sg.Text(key='text2', text='* 邮件服务器账号:', size=15, justification='right'),
            sg.Input(key='email_user', size=33, default_text=data['email_user'], tooltip='xxxx@ztccloud.com.cn'),
            sg.Text(key='text3', text='* 邮件服务器密码:', size=15, justification='right'),
            sg.Input(key='email_passwd', size=33, password_char='*', default_text=data['email_passwd'])
        ],
        # 第四行
        [
            sg.Text('邮件主题:', size=15, justification='right'),
            sg.Input(key='mail_title', size=33, default_text=data['mail_title']),
            sg.Text(key='text4', text='邮件收件人:', size=15, justification='right'),
            sg.Input(key='addressee', size=33, tooltip='多个收件人用逗号隔开,邮件将直接发送至收件人,请慎重',
                     default_text=data['addressee']),
            sg.Text(key='text5', text='邮件抄送人:', size=15, justification='right'),
            sg.Input(key='mail_cc', size=33, tooltip='抄送多人用逗号隔开', default_text=data['mail_cc'])
        ],
        # 按钮行
        [
            sg.Button("源码", size=10, mouseover_colors='#1d953f', use_ttk_buttons=True, right_click_menu=
            ['&Right', ['你的队友:', '【镇山的虎】', '【远见的鹰】', '【忠诚的狗】', '【善战的狼】',
                        '而你:', '【害群的马】', '【盛饭的桶】', '【墙头的草】', ['【卸载工具】'], '【搅屎的棍】']]),
            sg.Text('  ', size=92, justification='left'),  # 空白区域
            sg.Button("执行", bind_return_key=True, size=10, mouseover_colors='#1d953f', use_ttk_buttons=True),
            sg.Button("暂存", size=10, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='暂存界面输入内容'),
            sg.Button("清空", size=10, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='清空执行日志')
        ]
    ]
    """邮件正文组成部分输入区域"""
    layout_mail_text = [
        [
            sg.Text('邮件开篇:', size=15, justification='left')
        ],
        [
            sg.Multiline(key='daily_text', size=(75, 6), default_text=data['daily_text'])
        ],
        [
            sg.Text('里程碑总体进度:', size=13, justification='left'),
            sg.Input(key='milestone_address', size=47, default_text=data['milestone_address'],
                     tooltip='里程碑在线文档地址,用于展示'),
            sg.Button("贴图", size=4, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='将剪切板的截图贴到邮件里程碑位置'),
            sg.Button("预览", size=4, mouseover_colors='#dec674', use_ttk_buttons=True, tooltip='查看里程碑截图')
        ],
        [
            sg.Multiline(key='milestone_excel', size=(75, 4),
                         tooltip='可粘贴里程碑在线文档内容,请直接粘贴excel\ntip:\n    单元格合并格式只会保留第一列\n    单元格里边存在换行也会有异常..',
                         default_text=data['milestone_excel'].replace("_toolflag_", "	"))
        ],
        [
            sg.Text('今日测试执行清况:', size=15, justification='left'),
            sg.Input(key='manpower_today', size=3, default_text=data['manpower_today'], justification='right',
                     background_color='#009ad6'),
            sg.Text('人天', size=4, justification='left')
        ],
        [
            sg.Multiline(key='test_situation', size=(75, 4), default_text=data['test_situation'])
        ],
        [
            sg.Text('明日计划:', auto_size_text=True, justification='left')
        ],
        [
            sg.Multiline(key='tomorrow_plans', size=(75, 4), default_text=data['tomorrow_plans'])
        ]
    ]
    """工具执行结果展示控件"""
    layout_result = [
        [
            sg.Output(key='debug_result', size=(75, 26), echo_stdout_stderr=True)
        ]
    ]
    """总体布局容器"""
    layout2 = [
        [
            sg.Frame(key='frame1', title='', layout=layout)
        ],
        [
            sg.Frame(' 邮件内容:', layout_mail_text),
            sg.Frame(' 执行日志:', layout_result)
        ]
    ]

    return layout2

# 定时器UI
def timer_ui():
    layout = [
        [
            sg.Text('时:分:秒', size=(20, 2), font=('Helvetica', 14))
        ],
        [
            sg.Text('祝您顺利', size=(16, 2), font=('Helvetica', 20), justification='center', key='text_time')
        ],
        [
            sg.Button('开始计时', button_color=('white', '#001480')),
            sg.Button('重新计时', button_color=('white', '#007339')),
            sg.Button('中止发送', button_color=('white', 'firebrick4'))
        ]
    ]
    return layout


def GET_LIST(list1, values1):
    for i in list1:
        if isinstance(i, list):
            GET_LIST(i, values1)

        elif isinstance(i, dict):
            GET_DICT(i, values1)
        else:
            continue


def GET_DICT(dict1, values):
    """
    从响应中根据key获取对应的值(第一个)
    :param dict1: 响应结果json
    :param values: key
    :return: 查到的值(str)
    """
    global value  # 定义全局变量
    value = None
    values1 = values
    for k, v in dict1.items():
        if k == values:
            value = v
            break
        elif isinstance(v, list):  # 判断类型是不是list
            GET_LIST(v, values1)
        elif isinstance(v, dict):
            GET_DICT(v, values1)
        else:
            continue
    return value


# 从响应中根据key获取值(所有)
def GET_VALUE_FROM_JSON_DICT(_obj, key):
    """
    从响应中根据key获取值(所有)
    :param _obj: 响应结果json
    :param key: key
    :return: 查到的值(list)
    """
    ret = []

    def _get_value_from_json_dict(_obj, _key):
        if isinstance(_obj, list):
            for _i in _obj:
                _get_value_from_json_dict(_i, _key)
        elif isinstance(_obj, dict):
            for _k, _v in _obj.items():
                if _k == _key:
                    ret.append(_v)
                else:
                    _get_value_from_json_dict(_v, _key)
        else:
            return

    _get_value_from_json_dict(_obj, key)
    return ret


# 读配置文件-[TAPD]
def read_conf(path, TAPD_list):
    """
    读配置文件
    :param path: 配置文件路径
    :return: tmp=config.items("TAPD");list_value_conf
    """
    try:
        if not os.path.exists(path):  # 判断文件是否存在
            if not os.path.exists(path_pic):  # 判断文件夹是否存在
                os.makedirs(path_pic)
            tapdconf_reset(path)  # 重置配置文件
        else:
            with open(path, 'r') as f:
                conf_text = f.read()
            if len(conf_text.strip()) > 0:  # 判断是否空文件
                config.read(path)
                list_sections = config.sections()
                # 没这个类 # 参数不一样
                # print(f"{config.options('TAPD')}\n{TAPD_list}")
                if 'TAPD' not in list_sections or set(config.options('TAPD')) != set(TAPD_list):
                    if set(config.options('TAPD')) != set(TAPD_list):
                        print("------>字段不一样,重置配置文件了")
                    tapdconf_reset(path)  # 重置配置文件
            else:  # 空文件
                tapdconf_reset(path)  # 重置配置文件
        with open(path, 'r') as f:
            config.read_file(f)
            tmp = config.items("TAPD")  # 列表元组
            list_value_conf = {}  # 配置文件内容
            for k, v in tmp:
                list_value_conf[k] = v
            return list_value_conf
    except ValueError as error:
        print(f"读配置文件异常,存在异常编码,如'✓✉☏☞'等字符,即将重置配置文件。请重启工具\n异常信息:{error}")
        tapdconf_reset(path)  # 重置配置文件
    except Exception as error:
        if loglevel == "debug":
            raise error
        print(f"读配置文件异常,存在异常编码\n异常信息:{error}")
        tapdconf_reset(path)  # 重置配置文件
        pass


# 给section增加option:value
def add_option(path, section, option, value):
    # 给section增加option:value。
    # 如果section不存在就先增加一个section,option不存在就新增,存在就不操作
    # 不会影响已存在的section内的option
    try:
        with open(path, 'r') as f:
            config.read_file(f)
            if not config.has_section(section):
                config[section] = {
                    option: value
                }
            else:
                if not config.has_option(section, option):  # 不存在option
                    config.set(section, option, value)
            with open(path, 'w') as f2:
                config.write(f2)  # 写进文件
    except UnicodeError as error:  # 编码异常
        print(f"给{section}增加{option}:{value}失败,当前内容中包含特殊符号,如'✓✉☏☞',请处理后重新操作。\n错误信息:{error}")
    except Exception as error:
        print(f"给{section}增加{option}:{value}失败\n错误信息[7]:", error)


# 读section-option的值
def get_option(path, section, option):
    # 读section-option的值
    try:
        with open(path, 'r') as f:
            config.read_file(f)
            value = config.get(section, option)
            return value
    except Exception as e:
        print(f"读【{section}】-【{option}】失败!错误信息:{e}")
        pass


# 重置配置文件
def tapdconf_reset(path):
    """如果配置文件被改,调此方法直接重置配置文件"""
    config["TAPD"] = {
        'cookie': '',
        'url_tapd': '',
        'id': '',
        'start_time': '',
        'smtp_server': 'smtp.exmail.qq.com',
        'email_user': '@ztccloud.com.cn',
        'email_passwd': '',
        'iteration_id': '',
        'os1': '',
        'release_id': '',
        'testphase': '',
        'mail_title': '中兴新云测试日报',
        'b2': 'False',
        'b1': 'True',
        'addressee': '',
        'mail_cc': '',
        'daily_text': '',
        'milestone_address': '',
        'milestone_excel': '',
        'manpower_today': '',
        'test_situation': '',
        'tomorrow_plans': '',
        'list_os': "测试环境_toolflag_UAT环境_toolflag_预发布环境_toolflag_生产环境_toolflag_项目环境",
        'list_id': "产品_产品需求管理--67410840",
        'list_iteration': "",
        'list_testphase': "一轮测试_toolflag_二轮测试_toolflag_验收阶段_toolflag_线上阶段_toolflag_持续测试_toolflag_通测阶段",
        'list_release_id': "",
        'win2_flag': "N"
    }
    # select_mode = ['测试环境', 'UAT环境', '预发布环境', '生产环境', '项目环境']
    # test_phase = ['一轮测试', '二轮测试', '验收阶段', '线上阶段', '持续测试', '通测阶段']
    with open(path, 'w') as f:
        config.write(f)


# 写配置文件
def write_conf(path, section, option, value=None):
    """写配置文件"""
    try:
        config.set(section, option, value)  # 写配置文件
        with open(path, 'w') as f:
            config.write(f)
            # print("更新配置文件[%s]" % option)
    except UnicodeError as error:  # 编码异常
        print(f"[6]当前内容中包含特殊符号,如'✓✉☏☞',请处理后重新操作。\n无法解决请联系管理员,异常[键]-[值]为:"
              f"[{option}]-[{value}],请处理后【重新保存界面】。\n异常信息为:{error}")
    except Exception as error:
        print(f"写配置文件(write_conf)出错,写入的[键]-[值]为:[{option}]-[{value}]\n错误信息:{error}")


# 返回今天的±n个工作日
def tommorow(days=0, work_flag=True, end_time=datetime.date.today()):
    """
    工作日标志为True时,输入非0日期返回今天的±n个工作日,输入0直接返回今日日期;
    工作日标志为False时,输入非0日期返回今天的±n个日子,输入0直接返回今日日期
    默认取下一工作日
    :param days:
    :param work_flag: 工作日标志
    :param end_time: 测试截止日期,默认今天
    :return:
    """
    if b2 == "是":  # 是包含节假日
        work_flag = False
    if work_flag:
        return find_workday(int(days), end_time)
    else:
        return end_time + datetime.timedelta(days=int(days))


# 获取发布计划release_id列表
def get_release_id(cookie, id):
    """
    根据用户cookie和项目id获取发布计划列表
    """
    perpage = 20
    page = 1
    release_id_update_end = []
    while True:
        url = f'https://www.tapd.cn/{id}/releases/lists?page={page}&perpage={perpage}&sort_name=name&order=DESC'
        headers = {
            "accept": "*/*",
            "accept-encoding": "gzip, deflate, br",
            "accept-language": "zh-CN,zh;q=0.9",
            "cache-control": "no-cache",
            "cookie": cookie,
            "pragma": "no-cache",
            "referer": f'https://www.tapd.cn/{id}/releases/lists?page={page}&perpage={perpage}&sort_name=name&order=DESC',
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 SLBrowser/7.0.0.8031 SLBChan/103",
            "x-requested-with": "XMLHttpRequest"
        }
        try:
            r = requests.get(url=url, headers=headers)
            r.encoding = 'utf-8'
            if "<title>登录-TAPD</title>" in r.text:
                print("[TAPD_Cookie]无效,请重新输入cookie后再获取")
                return False
            list_released_data = r.text.replace('&amp;', '&')
            # 正则过滤出(发布计划id, 发布计划中文名),存储成俩列表
            release_id_update = re.findall(rf'<div id="operation_(.*?)" class="', list_released_data, re.S)
            released_name_update = re.findall(rf'<td title="(.*?)">', list_released_data, re.S)
            if len(release_id_update) > 0 and len(release_id_update) == len(released_name_update):
                for i in range(len(release_id_update)):
                    release_id_update_end.append(f"{released_name_update[i]}--{release_id_update[i]}")
            if len(release_id_update) < 20:
                return release_id_update_end
            page += 1
            # return r.text.replace('&amp;', '&')
        except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
            print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
            return False
        except Exception as err:
            print(f"获取数据失败,请检查字段信息[1].错误信息:{err}")
            return False


# 获取项目id列表 20220106还能用√
def tapd_id(cookie):
    """
    根据用户cookie获取项目id列表
    """
    url = 'https://www.tapd.cn/company/my_take_part_in_projects_list'
    headers = {
        "accept": "*/*",
        "accept-encoding": "gzip, deflate, br",
        "accept-language": "zh-CN,zh;q=0.9",
        "cache-control": "no-cache",
        "cookie": cookie,
        "pragma": "no-cache",
        "referer": "https://www.tapd.cn/67410840/prong/stories/stories_list",
        # https: // www.tapd.cn / 67410840 60519202/ prong / tasks
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 SLBrowser/7.0.0.8031 SLBChan/103",
        "x-requested-with": "XMLHttpRequest"
    }
    try:
        response = requests.get(url=url, headers=headers)
        if "<title>登录-TAPD</title>" in response.text:
            print("[TAPD_Cookie]无效,请重新输入cookie后再获取")
            return False
        list_object_id = re.findall(r"object-id=.(.*?)\">.*?<a title=\"(.*?)\"", response.text, re.S)
        for i in range(len(list_object_id)):
            list_object_id[i] = f'{list_object_id[i][1]}--{list_object_id[i][0]}'
        print("成功获取您参与的[项目]列表\n")
        return list_object_id
    except exceptions.ConnectTimeout as err:
        print("The request timed out while trying to connect to the remote server."
              "Requests that produced this error are safe to retry.\nerror_info:", err)
        return False
    except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
        print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
        return False
    except Exception as err:
        print("若网络正常,请检查TAPD_Cookie是否正常。\n错误信息:", err)
        return False


# 获取迭代版本
def get_options(cookie, workspace_ids):
    """
    传入版本id(workspace_ids),返回版本下的‘迭代版本名称-id’
    3.3壳子回炉重测(当前迭代)--1167410840001000517
    """
    page = 1
    perpage = 50
    iteration_list = []
    while True:
        url = "https://www.tapd.cn/api/new_filter/new_filter/get_options"
        headers = {
            "Content-Type": "application/json;charset=UTF-8",
            "Cookie": cookie,
            "user-agent": Faker().chrome(),
            "x-requested-with": "XMLHttpRequest"
        }
        data = {
            "entity_type": "common",
            "workspace_ids": workspace_ids,  # 67410840
            "field": "iteration_id",
            "is_system": 1,
            "use_scene": "search_filter",
            "Filter": {
                "iteration_id": {
                    "use_page": 1,
                    "Filter": {
                        "status": "~done"
                    },
                    "page": page,
                    "perpage": perpage
                }
            },
            "dsc_token": "bGdaKSgSBhk69NTJ"
        }

        try:
            response = requests.post(url=url, headers=headers, data=json.dumps(data))
            assert response.json().get("meta").get("code") == '0'
            iteration_name_list = GET_VALUE_FROM_JSON_DICT(response.json().get("data"), "label")
            iteration_id_list = GET_VALUE_FROM_JSON_DICT(response.json().get("data"), "value")

            if len(iteration_name_list) == len(iteration_id_list):
                for index, value in enumerate(iteration_name_list):
                    iteration_list.append(value + '--' + iteration_id_list[index])
            if len(iteration_name_list) >= perpage:
                page += 1
            else:
                return iteration_list
        except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
            print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
            return False
        except Exception as err:
            print(f"获取数据失败,请检查字段信息[2].错误信息:{err}")
            return False


# 获取缺陷字段用户视图(和缺陷列表) view_id, conf_id
def get_bug_fields_userview_and_list(cookie, workspace_ids, dsc_token='bGdaKSgSBhk69NTJ', query_token='', conf_id='',
                                     flag=None):
    """
    获取缺陷字段用户视图和缺陷列表 view_id, conf_id
    workspace_ids:项目id
    返回id、conf_id、当前显示字段
    flag:控制返回内容,默认返回全部响应json
    return:view_id, conf_id
    """
    url = "https://www.tapd.cn/api/aggregation/bug_aggregation/get_bug_fields_userview_and_list"
    headers = {
        "Content-Type": "application/json;charset=UTF-8",
        "Cookie": cookie,
        "user-agent": Faker().chrome(),
    }
    data = {
        "workspace_id": workspace_ids,
        "conf_id": conf_id,  # "1167410840001092960",
        "sort_name": "",
        "order": "",
        "perpage": 50,
        "page": "1",
        "selected_workspace_ids": "",
        "query_token": query_token,  # "90eb0b88b2d138fbbac9b079b1df6e2c",
        "location": "/bugtrace/bugreports/my_view",
        "target": f"{workspace_ids}/bug/normal",
        "entity_types": [
            "bug"
        ],
        "use_scene": "bug_list",
        "return_url": f"https://www.tapd.cn/tapd_fe/{workspace_ids}/bug/list?confId={conf_id}&page=1&queryToken={query_token}",
        "dsc_token": dsc_token
    }
    try:
        response = requests.post(url=url, headers=headers, data=json.dumps(data))
        assert response.status_code == 200
        # total_count = GET_VALUE_FROM_JSON_DICT(response.json(), "total_count")[0]
        # bugslist = GET_VALUE_FROM_JSON_DICT(response.json(), "bugslist")[0][0] if total_count != 0 else {}
        view_info = GET_VALUE_FROM_JSON_DICT(response.json(), "view_info")[0]

        view_id, conf_id = view_info.get("id"), view_info.get("conf_id")
        return view_id, conf_id
    except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
        print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
        return False
    except Exception as err:
        print(f"获取数据失败,请检查字段信息[3].错误信息:{err}")
        return False


# 条件查询获取缺陷列表by_url
def get_bugs_list_by_url(cookie, data=None, page=1, perpage=100, flag="bugs_list", **kwargs):
    """
    获取缺陷字段用户视图和缺陷列表
    """
    url = "https://www.tapd.cn/api/entity/bugs/bugs_list"
    headers = {
        "Content-Type": "application/json;charset=UTF-8",
        "Cookie": cookie,
        "user-agent": Faker().chrome(),
    }
    body = {}
    workspace_id = ''
    data = data.replace("true", "True").replace("false", "False").replace("null", "None")
    try:
        if isinstance(data, dict):
            try:
                body.update(data)
            except:
                sg.popup("请求非json[1],请检查", title='异常')
                return False
        if isinstance(data, str):
            try:
                body.update(eval(data))
            except:
                sg.popup("请求非json[2],请检查", title='异常')
                return False
        try:
            workspace_id = body["workspace_id"]
        except:
            sg.popup("请求非json[3],请检查", title='异常')
        body["page"] = page
        body["perpage"] = perpage
        response = requests.post(url=url, headers=headers, data=json.dumps(body))
        assert response.status_code == 200
        total_count = GET_VALUE_FROM_JSON_DICT(response.json(), "total_count")[0]  # str
        bugs_list = GET_VALUE_FROM_JSON_DICT(response.json(), "Bug") if total_count != 0 else {}  # dict
        if flag == "bugs_list":
            return bugs_list
        elif flag == "info":
            view_info = GET_VALUE_FROM_JSON_DICT(response.json(), "view_info")[0]
            view_id, conf_id = view_info.get("id"), view_info.get("conf_id")
            return view_id, conf_id, workspace_id, int(total_count)
        else:
            return int(total_count)
    except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
        print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
        return False
    except Exception as err:
        print(f"获取数据失败,请检查字段信息[4].错误信息:{err}")
        return False


# 条件查询获取缺陷列表
def get_bugs_list(cookie, workspace_id, conf_id, start_time='', end_time='', title='', iteration_id=None, os=None,
                  testphase=None,
                  release_id=None, query_token='', page=1, perpage=100, flag="bugs_list"):
    """
    获取缺陷字段用户视图和缺陷列表
    workspace_ids:项目id
    返回id、conf_id、当前显示字段
    flag:控制返回内容,默认返回全部响应json
    return:total_count:str; bugs_list:dict;
    """
    try:
        if release_id is None:
            release_id = []
        elif isinstance(release_id, str):
            release_id = [release_id]
        if testphase is None:
            testphase = []
        elif isinstance(testphase, str):
            testphase = [testphase]
        if os is None:
            os = []
        elif isinstance(os, str):
            os = [os]
        if iteration_id is None:
            iteration_id = []
        elif isinstance(iteration_id, str):
            iteration_id = [iteration_id]
        created = f"{start_time} 00:00,{end_time} 23:59"
        url = "https://www.tapd.cn/api/entity/bugs/bugs_list"
        headers = {
            "Content-Type": "application/json;charset=UTF-8",
            "Cookie": cookie,
            "user-agent": Faker().chrome(),
        }
        data = {
            "workspace_id": workspace_id,
            "conf_id": conf_id,
            "sort_name": "",
            "order": "",
            "perpage": perpage,
            "page": page,
            "selected_workspace_ids": [],
            "filter_expr": {
                "data": [
                    {"fieldOption": "like", "fieldType": "input", "fieldSystemName": "name", "fieldDisplayName": "标题",
                     "selectOption": [], "value": title, "fieldIsSystem": "1"},
                    {"fieldOption": "in", "fieldType": "select", "fieldSystemName": "iteration_id",
                     "fieldDisplayName": "迭代",
                     "selectOption": [], "value": iteration_id, "fieldIsSystem": "1"},
                    {"fieldOption": "between", "fieldType": "datetime", "fieldSystemName": "created",
                     "fieldDisplayName": "创建时间",
                     "selectOption": [], "value": created, "fieldIsSystem": "1"},
                    {"fieldOption": "in", "fieldType": "select", "fieldSystemName": "os", "fieldDisplayName": "操作系统",
                     "selectOption": [], "value": os, "fieldIsSystem": "1"},
                    {"fieldOption": "in", "fieldType": "select", "fieldSystemName": "testphase",
                     "fieldDisplayName": "测试阶段",
                     "selectOption": [], "value": testphase, "fieldIsSystem": "1"},
                    {"fieldOption": "in", "fieldType": "select", "fieldSystemName": "release_id",
                     "fieldDisplayName": "发布计划",
                     "selectOption": [], "value": release_id, "fieldIsSystem": "1"}
                ],
                "optionType": "AND",
                "needInit": True
            },
            "return_url": f"https://www.tapd.cn/tapd_fe/{workspace_id}/bug/list?page={page}&queryToken={query_token}",
            "dsc_token": "PzfsAcWwfGkyMA0s"
        }
        response = requests.post(url=url, headers=headers, data=json.dumps(data))
        assert response.status_code == 200
        total_count = GET_VALUE_FROM_JSON_DICT(response.json(), "total_count")[0]  # str
        bugs_list = GET_VALUE_FROM_JSON_DICT(response.json(), "Bug") if total_count != 0 else {}  # dict
        if flag == "bugs_list":
            return bugs_list
        else:
            return total_count

    except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
        print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
        return False
    except Exception as err:
        print(f"获取数据失败,请检查字段信息[5].错误信息:{err}")
        return False


# 加工数据成插入数据库的语句列表
def data_cleanup(data_tapd):
    """
    加工tapd缺陷数据,组装成insert语句
    :param data_tapd:
    :return: list_insert_sql: 插入数据库的语句列表
    """
    try:
        list_insert_sql = []
        for Bug in data_tapd:
            Bug["id"] = Bug["short_id"]
            Bug["status"] = Bug["status"].replace('new', '新').replace('rejected', '已拒绝').replace('resolved',
                                                                                                 '已解决').replace(
                'reopened', '重新打开').replace('verified', '已验证').replace('assigned', '确认产品bug').replace('closed', '已关闭')
            Bug["severity"] = Bug["severity"].replace('fatal', '致命').replace('serious', '严重').replace('normal', '一般')\
                .replace('prompt', '提示').replace('advice', '建议') or '一般'
            Bug["priority"] = Bug["priority"].replace("urgent", '紧急').replace("high", '高').replace("medium", '中')\
                .replace("low", '低').replace("insignificant", '无关紧要').replace("9999999", "-空-")
            Bug["created"] = re.sub(r'([0-9].*?) .{8}', r'\1', Bug["created"]).replace('--', '')
            Bug["resolved"] = re.sub(r'([0-9].*?) .{8}', r'\1', Bug["resolved"]).replace('--', '')
            Bug["closed"] = re.sub(r'([0-9].*?) .{8}', r'\1', Bug["closed"]).replace('--', '')
            Bug["reject_time"] = re.sub(r'([0-9].*?) .{8}', r'\1', Bug["reject_time"]).replace('--', '')
            Bug["reopen_time"] = re.sub(r'([0-9].*?) .{8}', r'\1', Bug["reopen_time"]).replace('--', '')
            Bug["title"] = Bug["title"].replace("'", '\\"').replace("/", "\\/").replace("%", "\\%").replace("[",
                                                                                                            "\\[").replace(
                "]", "\\[")
            Bug["custom_field_four"] = Bug.get("custom_field_four") or '0'  # reject_num
            Bug["reporter"] = Bug["reporter"].replace(';', '')
            Bug["de"] = Bug["de"].replace(';', '').replace('--', '') or Bug["reporter"]  # 不填开发就自己背着
            Bug["closer"] = Bug["closer"].replace(';', '').replace('--', '')
            Bug["fixer"] = Bug["fixer"].replace(';', '').replace('--', '')
            Bug["current_owner"] = Bug["current_owner"].replace(';', '').replace('--', '')
            sql_text = f"""INSERT INTO scores VALUES(
                        '{Bug["id"]}',
                        '{Bug["title"]}',
                        '{Bug["severity"]}',
                        '{Bug["status"]}',
                        '{Bug["current_owner"]}',
                        '{Bug["reporter"]}',
                        '{Bug["created"]}',
                        '{Bug["fixer"]}',
                        '{Bug["resolved"]}',
                        '{Bug["closed"]}',
                        '{Bug["reject_time"]}',
                        '{Bug["reopen_time"]}',
                        '{Bug["de"]}',
                        '{Bug["closer"]}',
                        '{Bug["priority"]}',
                        '{Bug["custom_field_four"]}')
                        """
            list_insert_sql.append(sql_text)
        return list_insert_sql
    except Exception as err:
        print(f"清洗数据失败,请联系管理员[1].错误信息:{err}")
        return False

# 获取缺陷显示字段
def get_show_fields(cookie, workspace_id, view_id):
    """
    获取缺陷显示字段拼接的str
    workspace_ids:项目id
    view_id:视图id
    return: "title;version_report;iteration_id;severity;priority;status;current_owner;reporter;created;bugtype;source;testtype;resolution;os;"
    """
    url = "https://www.tapd.cn/api/basic/userviews/get_show_fields"
    headers = {
        "Cookie": cookie,
        "user-agent": Faker().chrome(),
    }
    try:
        data = f'?id={view_id}&workspace_id={workspace_id}&location=/bugtrace/bugreports/my_view&form=show_fields'
        response = requests.get(url=url + data, headers=headers)
        assert response.status_code == 200
        field_list = GET_VALUE_FROM_JSON_DICT(response.json(), "fields")[0]
        return ';'.join(field_list)
    except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
        print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
        return False
    except Exception as err:
        print(f"获取数据失败,请检查字段信息[6].错误信息:{err}")
        return False

# 设置缺陷显示字段
def edit_show_fields(cookie, workspace_ids, view_id, custom_fields: str, dsc_token="bGdaKSgSBhk69NTJ"):
    """
    设置缺陷显示字段
    workspace_ids:项目id
    view_id:视图id
    custom_fields:视图显示字段
    dsc_token:不知道什么用,留个位置
    """
    url = "https://www.tapd.cn/api/basic/userviews/edit_show_fields"
    headers = {
        "Content-Type": "application/json;charset=UTF-8",
        "Cookie": cookie,
        "user-agent": Faker().chrome(),
    }
    try:
        data = {
            "workspace_id": workspace_ids,
            "id": view_id,
            "location": "/bugtrace/bugreports/my_view",
            "custom_fields": custom_fields,
            "dsc_token": dsc_token
        }
        response = requests.post(url=url, headers=headers, data=json.dumps(data))
        assert response.status_code == 200
        return True
    except (exceptions.ConnectionError, exceptions.RequestException) as err:  # 可解决fiddler、VPN代理造成的错误问题
        print("连接错误,请检查网络;确认是否未开启代理工具,如Fiddler、VPN。\nerror_info:", err)
        return False
    except Exception as err:
        print(f"设置缺陷显示字段失败,请检查字段信息[7].错误信息:{err}")
        return False

# 画图
def pict(tool, name, color):
    """
    用于画图
    :param tool: 数据列表,格式是[[x轴下标列表],label[0],label[1],label[2],label[...]]。
    x轴下标列表是一定会有的,后边也是列表,数据种类取自列表label,用来画柱子高度的,与x轴下标列表长度一样,一一对应,堆积往上画柱子。
    :param name: label标签列表
    :param color: local标签颜色列表
    """
    plt.rcParams['font.family'] = ['SimHei']  # 设置字体用于显示中文
    bottom = len(tool[0]) * [0]  # 堆积柱子的基底
    label = name  # label标签
    color = color  # 标签颜色
    i1 = 0  # label标签序列以及标签颜色序列
    # x=list(map(str,tool[0]))  # x轴
    x = tool[0]
    ab1 = plt.figure(figsize=(16, 6.5))  # 图形大小设置
    ab1.gca().spines["top"].set_color("none")  # 去图形除上边界
    ab1.gca().spines["left"].set_color("none")  # 去除图形左边界
    ab1.gca().spines["right"].set_color("none")  # 去除图形右边界
    plt.grid(axis='y', color='#dfdfdf')  # 设置图形网格线
    maxn = 0  # 最大数字
    for i in tool[1:]:
        if maxn < max(i):
            maxn = max(i)  # 更新最大数字
        plt.bar(x, i, bottom=bottom, label=label[i1], color=color[i1], zorder=10)  # 堆积设置柱子
        if len(tool) > 2:  # 如果是堆积柱子,则显示每个子柱子的数量,白色字体展示
            for a, b, c in zip(x, bottom, i):  # x:显示内容的x轴; botton+i/2:显示内容的y轴; i:显示的内容
                if c != 0: plt.text(a, b + c / 2, c, zorder=20, color='white')  # 如果i不等于0就显示每个堆积柱子的数字
        bottom = np.array(bottom) + np.array(i)  # 更新堆积柱子基底
        i1 += 1  # 更新label标签序列以及标签颜色序列
    if maxn == 0:  # 如果最大数字为0,说明没有一个柱子高度有值的,则没必要生成图片,退出函数
        print(f"图表【{name[-1]}】无数据,无需生成               <-----")
        return
    ab1.gca().yaxis.set_major_locator(MultipleLocator(math.ceil(maxn / 7)))  # 设置Y轴刻度间隔
    # plt.ylim(0, maxn+4)
    for a, b in zip(x, bottom):
        plt.text(a, b + 0.05, b, fontsize=12)  # 展示每列总数
    plt.title(name[-1], fontsize=16, y=1.1)  # 设置图形标题
    plt.legend(loc=(1.01, 0.9))  # 设置label标签位置
    if len(tool[0]) > 13:  # 如果x轴内容大于13,则x刻度旋转50度
        plt.xticks(rotation=50)
    print(f"生成【{name[-1]}】成功    √")
    path_savefig = path_pic + rf"{name[-1]}.jpg"  # 保存到跟配置文件一个路径下
    plt.savefig(path_savefig, bbox_inches='tight', pad_inches=0.3)  # 保存图片
    # plt.show()
    plt.close()


# 分析数据、存表、并画图
def analyze_datas(list_sql, list_date, id):
    """
    分析数据、存表、并画图
    :param list_sql: list_sql
    :param time_line: 时间轴
    :param id: 项目id
    :return:numbs:一些‘统计数据’以及两个‘html形式的清单’组成的列表
    """
    # 连接内存数据库
    conn = sqlite3.connect(':memory:')
    # 创建数据库游标
    cur = conn.cursor()
    today = end_time  # 邮件实际发送日期  # 全局变量
    # 建表的sql语句
    buginfo_table = '''CREATE TABLE scores
                   (id TEXT,
                    title TEXT,
                    severity TEXT,
                    status TEXT,
                    current_owner TEXT,
                    reporter TEXT,
                    created TEXT,
                    fixer TEXT,
                    resolved TEXT,
                    closed TEXT,
                    reject_time TEXT,
                    reopen_time TEXT,
                    de TEXT,
                    closer TEXT,
                    priority TEXT,
                    reject_num TEXT
                    );'''
    cur.execute(buginfo_table)  # 执行建表的sql语句

    for sql in list_sql:  # 插入缺陷数据
        cur.execute(sql)

    def exec(sql):  # 执行sql语句方法
        cur.execute(sql)
        return cur.fetchall()

    def get_numb(condition):  # 生成趋势图方法
        c3 = [0] * len(list_date)
        for index, date_choiced in enumerate(list_date):
            c3[index] += exec(f"SELECT count(*) from scores where {condition.replace('%date%', date_choiced)};")[0][0]
        return c3

    # 每日新增BUG数趋势图√
    pict([list_date, get_numb("created='%date%'")], ['新增缺陷数', '每日新增BUG数趋势图'], ['#63B8FF'])

    # pict([list_date, get_numb("closed='%date%'")], ['关闭缺陷数', '每日关闭缺陷数趋势图'], ['#63B8FF'])
    # pict([list_date, np.array(get_numb("status not in ('重新打开','新','确认产品bug') and  resolved='%date%'")) +
    #       np.array(get_numb("status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='%date%'")) +
    #       np.array(get_numb(
    #           "status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed='%date%'"))],
    #      ['修复缺陷数', '每日修复缺陷数趋势图'], ['#63B8FF'])

    # # 每日未关缺陷数趋势图
    # pict([list_date, get_numb("created<='%date%' and (closed>'%date%' or closed='')")], ['未关闭缺陷数', 'testphoto_每日未关缺陷数趋势图'],
    #      ['#63B8FF'])

    # 每日剩余BUG趋势图√
    # 每日未关闭总数量# 开发已解决待回归的# 开发未解决
    num_all, num_solved, num_new = [0] * len(list_date), [0] * len(list_date), [0] * len(list_date)
    for index, date_choiced in enumerate(list_date):
        date_choiced = str(date_choiced)
        # 保证创建时间早于等于当天
        # 保证当天还未关闭
        def add1times(reject_time, resolved, reopen_time, date_choiced):
            # 计算是否在这个日子里是不是处于处理了的状态
            if reopen_time == '':  # 重新打开时间为空
                if (resolved <= date_choiced and resolved != '') or (reject_time <= date_choiced and reject_time != ''):
                    return 1
            else:  # reopen_time!=''
                if reopen_time <= date_choiced:
                    if reopen_time <= reject_time <= date_choiced:
                        return 1
                    elif reopen_time <= resolved <= date_choiced:
                        return 1
                else:
                    if (reject_time <= date_choiced and reject_time != '') or (resolved <= date_choiced and resolved != ''):
                        return 1
            return 0
        sql_ads = f'''
            SELECT reject_time, resolved, reopen_time from scores where created<='{date_choiced}' 
            and (closed='' or closed>'{date_choiced}')
        '''
        listthis = exec(sql_ads)
        num_all[index] = len(listthis)
        num = 0
        for i in listthis:
            num += add1times(i[0], i[1], i[2], date_choiced)
        num_solved[index] = num
        if date_choiced == today:  # 最新一天的数据,基于特殊照顾,保证绝对准确
            sql_two = f'''
                SELECT count(*) from scores where created<='{date_choiced}'
                and status in ('已验证','已解决','已拒绝')
                and (closed='' or closed>'{date_choiced}')
            '''
            num_solved[index] = exec(sql_two)[0][0]
        num_new[index] += num_all[index] - num_solved[index]
    pict(tool=[list_date, num_solved, num_new], name=["已解决测试未关闭", "开发尚未解决", "每日剩余BUG趋势图"], color=["#90EE90", "#ed7158"])

    # today = '2021-08-20'
    def get_numb1(list_people, condition):
        """
        生成缺陷处理人/创建人分布图方法
        :param list_column: 人员列表,处理后用来替换【人员】flag1的
        :param factor_sql:统计缺陷数据的where条件语句
        :param label:where条件语句的其中一个条件的所有值;替换label[j]。最后一个元素存储图表名称用于pict方法画柱状图。
        :param color:传给pict方法画柱状图,柱子的颜色
        :return:
        """
        if len(list_people) == 0:
            print(f"图表【{label[-1]}】无数据,无需生成               <-----")
            return
        list_date = list(zip(*list_people))[0]
        c3 = [[[0] for _ in range(len(list_date))] for _ in range(len(label) - 1)]
        for i in range(len(list_date)):
            for j in range(len(label) - 1):
                sql_exec = f"SELECT count(*) from scores where {condition.replace('%date%', str(list_date[i]))}='{label[j]}';"
                c3[j][i] = exec(sql_exec)[0][0]
        pict([list(list_date)] + c3, label, color)
        # '[list(list_date)] + c3': 数据列表,格式是[[x轴下标列表], [严重缺陷个数列表], [一般缺陷个数列表], [建议缺陷个数列表]]。
        # x轴下标列表是一定会有的,在这就是人员列表,后边也是列表,数据种类取自列表label,用来画柱子高度的,与x轴下标列表长度一样,一一对应,堆积往上画柱子。
        return

    """测试数据"""
    # 测试今日创建BUG数分布图√
    label = ["建议", "提示", "一般", "严重", "致命", "今日创建BUG数分布图"]  # 标签参数,严重程度  # 顺序敏感
    color = ["#e7e5e5", "#90EE90", "#fed455", "#e9a661", "#ed7158"]  # 颜色参数
    sql_1 = f"select reporter from scores where created='{today}' group by reporter order by count(*) desc"  # 今天关闭的缺陷的创建人
    sql_2 = f"created='{today}' and reporter='%date%' and severity"
    get_numb1(exec(sql_1), sql_2)

    # 测试今日关闭BUG数分布图√
    label = ["已关闭", "今日关闭BUG数分布图"]  # 标签参数  # 顺序敏感
    color = ["#63B8FF"]  # 颜色参数
    sql_1 = f"select reporter from scores where closed='{today}' group by reporter order by count(*) desc"  # 今天关闭的缺陷的创建人
    sql_2 = f"closed='{today}' and reporter='%date%' and status"
    get_numb1(exec(sql_1), sql_2)
    # 测试创建BUG总数分布图√
    label = ["建议", "提示", "一般", "严重", "致命", "创建BUG总数分布图"]  # 标签参数,严重程度  # 顺序敏感
    color = ["#e7e5e5", "#90EE90", "#fed455", "#e9a661", "#ed7158"]  # 颜色参数
    sql_1 = f"select reporter from scores group by reporter order by count(*) desc"  # 今天关闭的缺陷的创建人
    sql_2 = f"reporter='%date%' and severity"
    get_numb1(exec(sql_1), sql_2)
    # 测试待处理BUG对应处理人分布图
    label = ["已验证", "已解决", "已拒绝", "待处理BUG对应处理人分布图"]  # 顺序敏感
    color = ["#90EE90", "#fed455", "#ed7158"]
    sql_1 = f"select current_owner from scores where status in ('已验证','已解决','已拒绝') group by current_owner order by count(*) desc"
    sql_2 = f"current_owner='%date%' and status"
    get_numb1(exec(sql_1), sql_2)

    """开发数据"""

    def get_numb2(list_sql, sql_2):  # 生成开发今日解决BUG数方法
        if len(list_sql) == 0:
            print(f"图表【{label[-1]}】无数据,无需生成               <-----")
            return
        list_date = list(zip(*list_sql))[0]
        c3 = [[[0] for _ in range(len(list_date))] for _ in range(len(label) - 1)]
        for i in range(len(list_date)):
            for j in range(len(label) - 1):
                c3[j][i] = \
                    exec(
                        f"SELECT count(*) from scores where {sql_2[j]}='{list_date[i]}';")[
                        0][0]
        pict([list(list_date)] + c3, label, color)
        return


    # 生成开发今日产生BUG数分布图
    label = ["转需求", "已拒绝", "已解决", "保持为新", "被重新打开", "今日产生BUG数分布图"]  # 标签参数  # 顺序敏感
    color = ["#90EE90", "#e7e5e5", "#63B8FF", "#fed455", "#ed7158"]  # 颜色参数
    sql_1 = f"select * from (select fixer as names from scores where status not in ('重新打开','新','确认产品bug') and created='{today}' and resolved!='' union all  select de as names from scores where status not in ('重新打开','新','确认产品bug') and created='{today}' and  resolved='' and reject_time!='' union all select closer as names from scores where status not in ('重新打开','新','确认产品bug') and created='{today}' and  resolved='' and reject_time='' and closed!='' union all select current_owner as names from scores where status in ('新','重新打开','确认产品bug') and created='{today}') group by names order by count(*) desc"
    sql_2 = [
        f"status not in ('重新打开','新','确认产品bug') and created='{today}' and  resolved='' and reject_time='' and closed!='' and closer",
        f"status not in ('重新打开','新','确认产品bug') and created='{today}' and resolved='' and reject_time!='' and de",
        f"status not in ('重新打开','新','确认产品bug') and created='{today}' and resolved!='' and fixer",
        f"status in ('新','确认产品bug') and created='{today}' and current_owner",
        f"status in ('重新打开') and created='{today}' and current_owner"]
    get_numb2(exec(sql_1), sql_2)

    # 生成开发今日解决BUG数分布图
    label = ["已解决", "已拒绝", "转需求", "今日解决BUG数分布图"]  # 标签参数  # 顺序敏感
    color = ["#63B8FF", "#e7e5e5", "#90EE90"]  # 颜色参数
    sql_1 = f"select * from (select fixer as names from scores where status not in ('重新打开','新','确认产品bug') and resolved='{today}' union all  select de from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='{today}' union all select closer from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed='{today}') group by names order by count(*) desc"
    sql_2 = [f"status not in ('重新打开','新','确认产品bug') and resolved='{today}' and fixer",
             f"status not in ('重新打开','新','确认产品bug') and resolved='' and reject_time='{today}' and de",
             f"status not in ('重新打开','新','确认产品bug') and resolved='' and reject_time='' and closed='{today}' and closer"]
    get_numb2(exec(sql_1), sql_2)

    # 生成开发累计解决BUG分布图
    label = ["已解决", "已拒绝", "转需求", "累计解决BUG分布图"]  # 标签参数  # 顺序敏感
    color = ["#63B8FF", "#e7e5e5", "#90EE90"]  # 颜色参数
    sql_1 = f"select * from (select fixer as names from scores where status not in ('重新打开','新','确认产品bug') and resolved!='' union all  select de from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time!='' union all select closer from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed!='') group by names order by count(*) desc"
    sql_2 = [f"status not in ('重新打开','新','确认产品bug') and resolved!='' and fixer",
             f"status not in ('重新打开','新','确认产品bug') and resolved='' and reject_time!='' and de",
             f"status not in ('重新打开','新','确认产品bug') and resolved='' and reject_time='' and closed!='' and closer"]
    get_numb2(exec(sql_1), sql_2)

    # 生成累计BUG对应处理人分布图
    label = ["转需求", "已拒绝", "已解决", "保持为新", "被重新打开", "累计BUG对应处理人分布图"]  # 标签参数  # 顺序敏感
    color = ["#90EE90", "#e7e5e5", "#63B8FF", "#fed455", "#ed7158"]  # 颜色参数
    sql_1 = f"select * from (select fixer as names from scores where status not in ('重新打开','新','确认产品bug') and resolved!='' union all  select de from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time!='' union all select closer from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed!='' union all select current_owner from scores where status in ('新','重新打开','确认产品bug')) group by names order by count(*) desc"
    sql_2 = [f"status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed!='' and closer",
             f"status not in ('重新打开','新','确认产品bug') and resolved='' and reject_time!='' and de",
             f"status not in ('重新打开','新','确认产品bug') and resolved!='' and fixer",
             f"status in ('新','确认产品bug') and current_owner",
             f"status in ('重新打开') and current_owner"]
    get_numb2(exec(sql_1), sql_2)

    # 生成累计BUG对应开发人员分布图
    label = ["转需求", "已拒绝", "已解决", "保持为新", "被重新打开", "累计BUG对应开发人员分布图"]  # 标签参数  # 顺序敏感
    color = ["#90EE90", "#e7e5e5", "#63B8FF", "#fed455", "#ed7158"]  # 颜色参数
    sql_1 = f"select * from (select de as names from scores where status not in ('重新打开','新','确认产品bug') and resolved!='' union all  select de as names from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time!='' union all select de as names from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed!='' union all select de as names from scores where status in ('新','重新打开','确认产品bug')) group by names order by count(*) desc"
    sql_2 = [f"status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed!='' and de",
             f"status not in ('重新打开','新','确认产品bug') and resolved='' and reject_time!='' and de",
             f"status not in ('重新打开','新','确认产品bug') and resolved!='' and de",
             f"status in ('新','确认产品bug') and de",
             f"status in ('重新打开') and de"]
    get_numb2(exec(sql_1), sql_2)

    # 重复打开缺陷个数排序
    label = ["返工一次", "返工两次", "两次以上", "重复打开BUG数对应开发人员分布图"]  # 标签参数  # 顺序敏感
    color = ["#63B8FF", "#fed455", "#ed7158"]  # 颜色参数
    sql_1 = f"select * from (select de as names from scores where reject_num not in ('0', 0, '', '--')) group by names order by count(*) desc"
    sql_2 = [f"reject_num = '1' and de",
             f"reject_num = '2' and de",
             f"reject_num not in ('0', 0, '', '--', '1', '2') and de"]
    get_numb2(exec(sql_1), sql_2)

    # 生成开发待解决BUG对应处理人分布图√
    if id == "67410840":  # 如果是产品迭代,则没有'确认产品bug'
        label = ["重新打开", "新", "待解决BUG对应处理人分布图"]  # 顺序敏感
        color = ["#ed7158", "#fed455"]
        sql_1 = f"select current_owner from scores where status in ('重新打开','新')  group by current_owner order by count(*) desc"
        sql_2 = f"current_owner='%date%' and status"
        get_numb1(exec(sql_1), sql_2)  # 待解决缺陷对应处理人分布图
    else:  # 其他项目则存在确认产品bug状态的缺陷
        label = ["重新打开", "确认产品bug", "新", "待解决BUG对应处理人分布图"]  # 顺序敏感
        color = ["#ed7158", "#e7e5e5", "#fed455"]
        sql_1 = f"select current_owner from scores where status in ('重新打开','确认产品bug','新')  group by current_owner order by count(*) desc"
        sql_2 = f"current_owner='%date%' and status"
        get_numb1(exec(sql_1), sql_2)  # 待解决缺陷对应处理人分布图

    def get_numb3(list_sql, pic_name):
        """
        生成重新打开缺陷清单、阻塞缺陷清单的表格
        :param list_sql: 查询表格数据sql
        :param pic_name: 图表名称
        :return:
        """
        html = '<table border=1 cellpadding="6px"><tr bgcolor="#9bdde9"><td style=\'width:80px\' align=\'center\'>缺陷ID</td><td style=\'width:450px\' align=\'center\'>缺陷标题</td><td style=\'width:120px\' align=\'center\'>缺陷处理人</td><td style=\'width:120px\' align=\'center\'>缺陷创建人</td></tr>'
        if len(list_sql) == 0:
            print(f"图表【{pic_name}】无数据,无需生成               <-----")
            html = '<p>暂无</p>'
        else:
            print(f"生成【{pic_name}】成功    √")
            for i in range(len(list_sql)):
                html = f'{html}<tr><td align=\'center\' \'width:80px\'>{list_sql[i][0]}</td><td style=\'width:450px\'>{list_sql[i][1]}</td><td style=\'width:120px\' align=\'center\'>{list_sql[i][2]}</td><td style=\'width:120px\' align=\'center\'>{list_sql[i][3]}</td></tr>'
            html = html + '</table>'  # f'{html}</table>'
        return html

    def get_numb4(list_sql, pic_name):
        """
        生成阻塞缺陷清单的表格
        :param list_sql: 查询表格数据sql
        :param pic_name: 图表名称
        :return:
        """
        html = '<table border=1 cellpadding="6px"><tr bgcolor="#9bdde9"><td style=\'width:80px\' align=\'center\'>缺陷ID</td><td style=\'width:450px\' align=\'center\'>缺陷标题</td><td style=\'width:120px\' align=\'center\'>缺陷处理人</td><td style=\'width:120px\' align=\'center\'>缺陷创建人</td><td style=\'width:120px\' align=\'center\'>阻塞天数</td></tr>'
        if len(list_sql) == 0:
            print(f"图表【{pic_name}】无数据,无需生成               <-----")
            html = '<p>暂无</p>'
        else:
            print(f"生成【{pic_name}】成功    √")
            for i in range(len(list_sql)):
                daysed = (datetime.datetime.strptime(today, "%Y-%m-%d") - datetime.datetime.strptime(list_sql[i][4],
                                                                                                     "%Y-%m-%d")).days
                html = f'{html}<tr><td align=\'center\' \'width:80px\'>{list_sql[i][0]}</td><td style=\'width:450px\'>{list_sql[i][1]}</td><td style=\'width:120px\' align=\'center\'>{list_sql[i][2]}</td><td style=\'width:120px\' align=\'center\'>{list_sql[i][3]}</td><td style=\'width:120px\' align=\'center\'>{str(daysed)}</td></tr>'
            html = html + '</table>'  # f'{html}</table>'
        return html

    # 生成阻塞缺陷清单
    sql_1 = "select id,title,current_owner,reporter,created from scores where priority in ('紧急','高') and status in ('重新打开','确认产品bug','新') order by created;"
    mail1 = get_numb4(exec(sql_1), '阻塞缺陷清单')
    # 生成重新打开缺陷清单
    sql_1 = "select id,title,current_owner,reporter from scores where status='重新打开';"
    mail2 = get_numb3(exec(sql_1), '重新打开缺陷清单')
    numbs = []
    numbs.append(exec("select count(*) from scores;")[0][0])  # 缺陷总数 1
    numbs.append(exec(
        f"select count(*) from (select fixer as names from scores where status not in ('重新打开','新','确认产品bug') and resolved='{today}' union all  select de from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='{today}' union all select closer from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed='{today}');")[
                     0][0])  # 今日开发解决bug数 2
    numbs.append(exec(
        "select count(*) from (select fixer as names from scores where status not in ('重新打开','新','确认产品bug') and resolved!='' union all  select de from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time!='' union all select closer from scores where status not in ('重新打开','新','确认产品bug') and  resolved='' and reject_time='' and closed!='');")[
                     0][0])  # 开发累计解决bug数 3
    numbs.append(
        exec("select count(*) from scores where priority in ('紧急','高') and status in ('重新打开','确认产品bug','新');")[0][
            0])  # 阻塞问题总数 4
    numbs.append(mail1)  # 阻塞问题 5
    numbs.append(exec("select count(*) from scores where status='重新打开'")[0][0])  # 重新打开问题总数 6
    numbs.append(mail2)  # 重新打开缺陷清单 7
    numbs.append(exec(f"select count(*) from scores where created='{today}' ;")[0][0])  # 今日测试提交缺陷数 8
    numbs.append(exec(f"select count(*) from scores where status in ('已验证','已解决','已拒绝') ;")[0][0])  # 截止目前待测试回归缺陷数 9
    numbs.append(exec(f"select count(*) from scores where status in ('重新打开','确认产品bug','新') ;")[0][0])  # 截止目前待开发解决缺陷数 10
    numbs.append(exec(f"select count(*) from scores where closed='{today}';")[0][0])  # 今日测试关闭数 11
    numbs.append(exec(f"select count(*) from scores where reject_num not in ('0', 0, '', '--');")[0][0])  # 重复打开缺陷数 12
    # 关闭游标
    cur.close()
    # 关闭连接
    conn.close()
    return numbs


# 获取日期 b2:是否包含节假日
def get_date(start_time, end_time, has_holiday):
    """
    根据是否过滤节假日标志,返回起止日期之间的所有日期
    :param start_time:起日期
    :param end_time:止日期
    :param has_holiday: 是否包含节假日,是/否,str
    :return:list_date:日期列表
    """
    # return:time_line:时间轴列表
    # end_time = datetime.date.today() if end_time == '' else datetime.date(*map(int, end_time.split("-")))
    list_date = []
    start_time = datetime.date(*map(int, start_time.split("-")))
    end_time = datetime.date(*map(int, end_time.split("-")))  # 使用邮件实际发送日期赋值  end_time:全局变量
    if end_time < start_time:
        end_time = datetime.date(*map(int, str(datetime.date.today()).split("-")))  # 取今天的end_time

    if has_holiday == "是":
        list_date.extend([str(i) for i in get_dates(start_time, end_time)])
        # while start_time <= end_time:
        #     list_date.append(str(start_time))
        #     start_time = start_time + datetime.timedelta(days=1)
        #     continue
    else:
        list_date.extend([str(i) for i in get_workdays(start_time, end_time)])
        # while start_time <= end_time:
        #     if is_workday(start_time):
        #         list_date.append(str(start_time))
        #     start_time = start_time + datetime.timedelta(days=1)
        #     continue
    return list_date


# 解析邮件地址,以保证邮有别名可以显示
def __format_addr__(addr):
    # 解析邮件地址,以保证邮有别名可以显示
    alias_name, addr = parseaddr(addr)
    # 防止中文问题,进行转码处理,并格式化为str返回
    return formataddr((Header(alias_name, charset="utf-8").encode(),
                       addr))


# 发送邮件方法
def send_mail(user, passwd, list_addressee, list_mail_cc, smtp_server, numbs, list_value):
    """

    :param user: 发件人账号
    :param passwd: 发件人密码
    :param addressee: 收件人账号列表:list
    :param mail_cc: 抄送人账号列表:list
    :param smtp_server: 邮箱服务器地址
    :param numbs:一些‘统计数据’以及两个‘html形式的清单’组成的列表
    :param list_value:
    :return:
    """

    def check1(image_name, site):  # 检测图片是否存在,根据结果选择HTML代码
        """
        path_pic:图片存储路径,全局变量
        image_name:图片名称
        site:用于生成html的图片展位符
        """
        if os.path.exists(path_pic + image_name):
            return f'<p><img src="cid:{site}"></p>'
        else:
            if image_name == '里程碑截图.jpg':
                return ''
            elif image_name == '每日新增BUG数趋势图.jpg':
                return '<p>暂无BUG记录</p>'
            else:
                return '<p>暂无</p>'

    # 邮件实际发送日期的下一个工作日
    end_time_int = datetime.date(*map(int, end_time.split("-")))
    next_workday = end_time_int + datetime.timedelta(days=int(1))
    if list_date_Source == "chinese_calendar":
        while is_holiday(next_workday):
            next_workday = next_workday + datetime.timedelta(days=int(1))

    html_style = '''
    <style>
			#mingpian{
				width:350px;
				height:110px;
				border-radius:5px;
				background-color:#2F4F4F;
				font-family:"微软雅黑";
			}
			#msg {
				width:350px;
				height:30px;
				font-size:16px;
				color:#000000	;
				padding-top:10px;
				background-color:#F5F5F5;
			}
			#msg mail{
				float:center;
				font-family:STLiti,FZShuTi,STHupo,STXinwei,"Simhei",STKaiti;
				font-size:22px;
				font-weight:bold;	
			}
			#logoimg{
				padding-top:10px;
				float:center;
				width:350px;
				height:80px;
			}
			#logoimg img{
				width:200px;
				height:30px;
			}
			.clearDiv{clear:both;}
		</style>
'''
    # <pre>{list_value[5]}<br/></pre>
    # image13
    html = f'''
    <html>
    <head></head>
    <body>
    <pre>{list_value[0]}</pre>
    <hr style="height:1px;border:none;border-top:1px solid #DCDCDC;" align="left"/>
    <h2>一、里程碑总体进度</h2>
    <a href={list_value[4]} target="_blank" title={list_value[4]}>{list_value[4]}</a><br/>
    {list_value[5]}
    {check1('里程碑截图.jpg', 'image8')}
    <h2>二、每日BUG趋势</h2>
    <p><b>每日新增缺陷趋势,期间累计总数: {numbs[0]}</b></p>
    {check1('每日新增BUG数趋势图.jpg', 'image1')}    
    <p><b>每日剩余缺陷趋势:{numbs[8]+numbs[9]}</b></p>
    {check1('每日剩余BUG趋势图.jpg', 'image14')}
    <h2>三、阻塞问题:{numbs[3]}</h2>
    {numbs[4]}
    <h2>四、待解决的重新打开状态BUG:{numbs[5]},已定性的返工bug数共计:{numbs[11]}</h2>
    {numbs[6]}
    {check1('重复打开BUG数对应开发人员分布图.jpg', 'image13')}
    <h2>五、今日测试投入情况:{list_value[3]}人天</h2>
    <pre>{list_value[1]}</pre>
    <h2>六、开发+需求缺陷数据统计</h2>
    <p><b>今日新增BUG:{numbs[7]}</b></p>
    {check1('今日产生BUG数分布图.jpg', 'image4')}
    <p><b>今日解决BUG:{numbs[1]}</b></p>
    {check1('今日解决BUG数分布图.jpg', 'image3')}
    <p><b>待解决BUG:{numbs[9]}</b></p>
    {check1('待解决BUG对应处理人分布图.jpg', 'image2')}
    <p><b>累计解决BUG:{numbs[2]}</b></p>
    {check1('累计解决BUG分布图.jpg', 'image5')}
    <p><b>累计BUG:{numbs[0]}</b></p>
    {check1('累计BUG对应处理人分布图.jpg', 'image6')}
    {check1('累计BUG对应开发人员分布图.jpg', 'image12')}
    <h2>七、测试缺陷数据统计</h2>
    <p><b>今日创建BUG:{numbs[7]}</b></p>
    {check1('今日创建BUG数分布图.jpg', 'image7')}
    <p><b>今日关闭BUG:{numbs[10]}</b></p>
    {check1('今日关闭BUG数分布图.jpg', 'image9')}
    <p><b>待处理BUG:{numbs[8]}</b></p>
    {check1('待处理BUG对应处理人分布图.jpg', 'image10')}
    <p><b>测试创建BUG总数:{numbs[0]}</b></p>
    {check1('创建BUG总数分布图.jpg', 'image11')}
    <hr style="height:1px;border:none;border-top:1px solid #DCDCDC;" align="left"/>
    <h2>明日({next_workday})测试计划:</h2>
    <pre>{list_value[2]}</pre>
    <br />
    <br />
	<hr style="height:1px;border:none;border-top:1px solid #B5C4DF;" width="350" align="left"/>
	<body align="left">
		<div id="mingpian"  align="center">
			<div id="msg">
				<a><mail>{user}</mail></a>
			</div>
			<div id="logoimg">
				<a href="http://ztessc.cn/"><img src="http://qty83k.creatby.com/materials/33005/hd/5792bc4acc4bc84aeb64f29795ef5561_4096.png"/></a>
		    </div>
            {html_style}
			<div class="clearDiv"></div>
		</div>
	</body>
    </body>
    </html>
    <style>
        table,table tr th, table tr td {{ border:1px solid #0094ff; }}
        table {{ border-collapse: collapse; }}  
    </style>
    '''
    smtp_server = smtp_server
    SMTP_PORT = 25
    msg = MIMEMultipart()
    msg.attach(MIMEText(html, 'html', 'utf-8'))

    def addimages(image_location, image_id):
        fp = open(image_location, 'rb')
        msgImage = MIMEImage(fp.read())
        fp.close()
        msgImage.add_header('Content-ID', image_id)
        return msgImage

    # 按位置给邮件插入图片
    def check(a, b):  # 如果图片存在,则添加图片到邮件
        if os.path.exists(a):
            msg.attach(addimages(a, b))
        else:
            return
    check(path_pic + '每日新增BUG数趋势图.jpg', 'image1')
    check(path_pic + '每日剩余BUG趋势图.jpg', 'image14')
    check(path_pic + '里程碑截图.jpg', 'image8')
    # 开发
    check(path_pic + '待解决BUG对应处理人分布图.jpg', 'image2')
    check(path_pic + '今日解决BUG数分布图.jpg', 'image3')
    check(path_pic + '今日产生BUG数分布图.jpg', 'image4')
    check(path_pic + '累计解决BUG分布图.jpg', 'image5')
    check(path_pic + '累计BUG对应处理人分布图.jpg', 'image6')
    check(path_pic + '累计BUG对应开发人员分布图.jpg', 'image12')
    check(path_pic + '重复打开BUG数对应开发人员分布图.jpg', 'image13')
    # 测试
    check(path_pic + '今日创建BUG数分布图.jpg', 'image7')
    check(path_pic + '今日关闭BUG数分布图.jpg', 'image9')
    check(path_pic + '待处理BUG对应处理人分布图.jpg', 'image10')
    check(path_pic + '创建BUG总数分布图.jpg', 'image11')
    # 格式化展示收件人
    strFrom = __format_addr__(user)
    msg['From'] = strFrom

    # 格式化展示发件人
    strTo = list()
    # 原来是一个纯邮箱的list,现在如果是一个["jaychen<jaychen@jay.com>"]的list给他格式化
    try:
        for a in list_addressee:
            strTo.append(__format_addr__(a))
    except Exception as e:
        # 没有对a和toadd进行type判断,出错就直接还原
        strTo = list_addressee
    msg['To'] = ','.join(strTo)  # 这个一定要是一个str,不然会报错“AttributeError: 'list' object has no attribute 'lstrip'”

    # 格式化展示抄送人
    strCC = list()
    # 原来是一个纯邮箱的list,现在如果是一个["jaychen<jaychen@jay.com>"]的list给他格式化
    try:
        for a in list_mail_cc:
            if a not in list_addressee:
                strCC.append(__format_addr__(a))
    except Exception as e:
        # 没有对a和toadd进行type判断,出错就直接还原
        strCC = list_mail_cc
    msg['cc'] = ','.join(strCC)

    # msg['To'] = formataddr(("收件人", user))  # 括号里的对应收件人邮箱昵称、收件人邮箱账号
    # msg['cc']=cc
    msg['Subject'] = list_value[6]  # "中兴新云测试日报"  # 构建msg对象的邮件主题

    def del_img(f):
        return f[-4:] == ".jpg"

    try:
        smtp_server = smtplib.SMTP(smtp_server, SMTP_PORT)  # 连接SMTP服务器
    except Exception as err:
        print('\n邮件发送失败,请确认[邮件服务器地址]是否可连接。\n错误信息:{0}'.format(err))
        return
    flag = False  # 返回发件结果
    try:
        smtp_server.ehlo()
        smtp_server.starttls()
        smtp_server.ehlo()
        smtp_server.login(user, passwd)  # 身份认证
        addressee_all = list_addressee + list_mail_cc  # 最终收件人=收件人+抄送人
        addressee_all = list(set(addressee_all))  # list去重
        smtp_server.sendmail(user, addressee_all, msg.as_string())  # 利用msg的as_string()方法转换成字符串
        print('\n发送邮件成功,请前往邮箱:【{0}】已发送邮件中查看'.format(user))
        flag = True
    except smtplib.SMTPAuthenticationError as err:
        print('\n邮箱身份认证失败,请确认[邮件服务器账号/密码]是否正确。\n错误信息:{0}\n'.format(err))
    except smtplib.SMTPSenderRefused as err:
        print('\nSender address refused.\n错误信息:{0}\n'.format(err))
    except smtplib.SMTPDataError as err:
        print('\nSMTP 服务器不接受数据。\n错误信息:{0}\n'.format(err))
    except smtplib.SMTPConnectError as err:
        print('\n连接建立期间出错。\n错误信息:{0}\n'.format(err))
    except Exception as err:
        if isinstance(err, UnicodeEncodeError):
            print("\n邮件发送失败! [邮件服务器账号/密码/收件人/抄送人]输入了异常字符,比如中文。\n错误信息:", err)
        else:
            print('\n邮件发送失败,请确认邮箱配置。\n错误信息:{0}\n'.format(err))
    finally:
        smtp_server.quit()
        # 删除图片
        del_img1 = list(filter(del_img, os.listdir(path_pic)))  # 过滤出要删的图片列表
        for i in del_img1:  # 删除图片
            if 'testphoto_' not in i:  # 测试图片带testphoto_的不删
                i = path_pic + i
                os.remove(i)
        if flag:
            return True


# 模糊匹配方法
def search_result(list_old, str):
    """
    根据str筛选列表各项,返回包含str的项组成的新列表
    list_old:原列表;
    str:过滤字段,返回过滤后的字段
    return 筛选数据后的列表
    """
    return list((filter(lambda seq: re.search(str, seq) is not None, list_old)))


# 输入框格式检查
def input_check(key_data):
    if key_data.isdigit() is False:  # 不是纯数字
        return True
    return False


# 测试开始时间校验
def date_check(date):
    """
    校验date格式为 YYYY-mm-dd,且早于当天日期。超过120天弹提示框。
    如未符合以上所有条件,返回True
    """
    try:
        today = datetime.datetime.today().strftime('%Y-%m-%d')  # 获取今日日期:str
        today = datetime.datetime.strptime(today, "%Y-%m-%d")  # str转datetime
        if len(date) == 10:
            r = re.compile('^\d{4}-\d{2}-\d{2}')  # 正则规则,yyyy-mm-dd
            if r.match(date) is None:
                sg.popup("请确认[测试开始时间]格式正确", title='提示')
                return True
        else:
            sg.popup("请确认[测试开始时间]格式正确", title='提示')
            return True
        start_time_datetime = datetime.datetime.strptime(date, "%Y-%m-%d")  # str转datetime
        if (start_time_datetime - today).days > 0:
            sg.popup("[测试开始时间]请选择今日之前的日期", title='提示')
            return True
        if (start_time_datetime - today).days < -120:
            button_return = sg.popup_ok_cancel("测试开始日期距今超过120天,\n会影响图表展示效果,\n是否继续?")
            if button_return == 'Cancel' or button_return is None:
                return True
    except Exception:
        sg.popup("请检查[测试开始时间]格式是否正确", title='提示')


# 检查网络状态
def bad_net(url='https://www.baidu.com'):
    r = requests.get(url, timeout=3)
    # assert r.status_code == 200, r.status_code


# 检查网络状态
def checkNet_process():
    """检查网络状态"""
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0',
    }
    try:
        response = requests.head('https://www.tapd.cn/company/participant_projects?from=left_tree2', headers=headers,
                                 timeout=5)  # url, headers=headers)
        if response.status_code != 200:
            print('网络无法访问,状态码:{0}'.format(response.status_code))
        # else:
        #     print('网络正常访问')
    except requests.exceptions.ConnectTimeout as error:
        sg.popup("网络连接超时!\n请确保代理工具如Fiddler、VPN等关闭、网络正常", title='警告')
        print('网络连接超时,请确保代理工具如Fiddler、VPN等关闭、网络正常。\n错误信息:', error)
        pass
    except requests.exceptions.ConnectionError as error:
        sg.popup("主机网络异常!\n请确保代理工具如Fiddler、VPN等关闭、网络正常", title='警告')
        print('主机网络异常,请确保代理工具如Fiddler、VPN等关闭、网络正常。\n错误信息:', error)
        pass
    except Exception as error:
        sg.popup("主机网络异常!\n将导致程序网络响应阻塞", title='警告')
        print('网络异常报错,请确保代理工具如Fiddler、VPN等关闭、网络正常。\n错误信息:', error)


# 获取应该合并行数
def get_n_num(list, idx):
    """获取首列应该合并行数"""
    num = 1  # 默认合并一行
    for i in list[idx + 1:]:
        if i[0] == '':
            num += 1
        else:
            return num
    return num


# 创建html中的table标签
def text_to_table(milestone_list, milestone_excel):
    """
    创建html中的table标签
    :param milestone_list: 一个以行元素组成的列表为元素的列表,list[list,list]
    :param milestone_excel: 里程碑内容输入框的原始值,string
    :return: html的table标签的内容,text
    """
    td_line = ''
    num_row = 0  # 存第一行的列数
    num = 0  # 存最后一行的列数与第一行的差
    table_text = '<table border="1" bordercolor="#000000" cellspacing="0" cellpadding="2" style="border-collapse:collapse;">'  # 存储table标签

    list_n_num = []
    for i_1 in milestone_list:
        # 获得一个子项为列表的列表
        tr_list_t = i_1.split('\t')
        list_n_num.append(tr_list_t)

    for inx1, val in enumerate(list_n_num):  # 遍历列表
        if inx1 == 0:
            num_row = len(val)  # 取第一列的列数作为列数
            if num_row < 3:  # 列数少于3,也认为不是里程碑内容,直接返回输入框内容
                table_text = milestone_excel
                return table_text
        for inx2, val2 in enumerate(val):  # 遍历列表的子项,也是列表
            if inx1 == 0:  # 第一行:粗体,颜色亮蓝
                val2 = "<th bgcolor=#87CEFA>" + val2 + "</th>"
            elif inx1 != 0 and inx2 == 0:  # 第二行开始,第一列
                if inx1 == 1 or val2 != '':  # 第二行 或者 第一列有值,满足一个,准备吞并
                    n_num = get_n_num(list_n_num, inx1)
                    val2 = "<td rowspan=" + str(n_num) + ">" + val2 + "</th>"
            else:
                val2 = "<td>" + val2 + "</td>"
            td_line = td_line + val2  # 一行的值
            num = num_row - len(val)  # 存最后一行的列数与第一行的差
        if num != 0:  # 列数不等于第一列的话,自动补齐空格
            td_line = td_line + "<td></td>" * num
        td_line = "<tr>" + td_line + "</tr>"
        table_text = table_text + td_line
        td_line = ''
    table_text = "<pre>" + table_text + "</table><br/></pre>"
    return table_text


# 控制系统配置区域控件的展示与隐藏[未实现]
def display_control(window, data):
    try:
        if window[event].get_text() == '︽':
            # 折叠
            window[event].Update(text='︾')
            window['text1'].update(visible=False)
            window['smtp_server'].update(visible=False)
            window['text2'].update(visible=False)
            window['email_user'].update(visible=False)
            window['text3'].update(visible=False)
            window['email_passwd'].update(visible=False)
            # window['frame1'].update(value=layout_factor)

        else:
            # 展开
            window[event].Update(text='︽')
            window['text1'].update(visible=True)
            window['smtp_server'].update(visible=True)
            window['text2'].update(visible=True)
            window['email_user'].update(visible=True)
            window['text3'].update(visible=True)
            window['email_passwd'].update(visible=True)
    except:
        pass


# 将界面字段值写入配置文件
def save_data(values, list_value_conf, flag_UpdateConf=False):
    """

    :param values: tool界面的值,类dict
    :param list_value_conf: 配置文件内实时内容键值对,list
    :param flag_UpdateConf: 是否更新配置文件标志,默认False
    """
    try:
        data_conf = copy.deepcopy(list_value_conf)
        move = dict.fromkeys((ord(c) for c in u"\xa0\n\t"))
        for key in values:
            values[key] = str(values[key]).translate(move)
            values[key] = unicodedata.normalize('NFKC', str(values[key]))
            if key in ['b1', 'b2']:
                values[key] = str(values[key])
            if key not in TAPD_list:
                continue
            elif values[key] == data_conf[key]:  # 界面内容vals2与配置文件内容list_value_conf一致
                continue
            else:
                if key == 'milestone_excel':  # 处理存储ini后空格制表符差异问题
                    values[key] = values['milestone_excel'].replace("	", "_toolflag_")
                # config.set("TAPD", key, str(values[key]))  # 写入配置文件
                unistr = unicodedata.normalize('NFKC', str(values[key]))
                config.set("TAPD", key, unistr)  # 写入配置文件
                data_conf[key] = unistr
                flag_UpdateConf = True
        if flag_UpdateConf:
            with open(path, 'w') as f1:
                config.write(f1)
        print("[%s]界面信息已成功保存,当前数据在工具启动时可自动恢复。" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
    except UnicodeError as error:  # 编码异常
        print(f"[7]当前界面信息中包含特殊符号,如'✓✉☏☞',请处理后【重新操作】!\n异常信息为:{error}")
        # raise error
    except Exception as error:
        print(f"将界面字段值写入配置文件异常,异常信息为:{error}")
    else:  # 无异常,更新list_value_conf
        # global list_value_conf
        list_value_conf = data_conf


# 预览里程碑截图
def show_figure(path_milestone):
    """预览里程碑截图"""
    try:
        if os.path.exists(path_milestone):
            # import matplotlib
            # matplotlib.use('tkagg')
            # import matplotlib.pyplot as plt
            #
            fig = plt.figure('里程碑进度图片')
            # plt.title('里程碑进度图片')
            ax = fig.add_axes([0, 0, 1, 1])
            ax.axis('off')

            ax.imshow(plt.imread(path_milestone), aspect='equal')
            # Tk specific!
            fig.canvas.toolbar.pack_forget()
            # plt.axis('off')  # 关掉坐标轴为 off
            # plt.ion()  # 开启interactive mode
            plt.show()
            plt.close()

            # from PIL import Image
            # plt.axis('off')  # 关掉坐标轴为 off
            # img = Image.open(path_milestone)
            # plt.ion()  # 开启interactive mode
            # plt.figure('里程碑进度图片')  # 创建图表 里程碑进度图片
            # plt.imshow(img)
            # plt.show()
        else:
            sg.popup("不存在【里程碑进度图片】\n请先贴图", title='提示')
    except Exception as error:
        if os.path.exists(path_milestone):
            os.remove(path_milestone)
        print("查看【里程碑进度图片】异常,错误信息:", error)


# 定时器结果通知
def timer_result_mail(smtp_server, email_user, email_passwd, flag):
    """定时器发邮件失败了,通知发件人,flag:0成1败"""
    if flag == 0:
        flag = "成功"
    else:
        flag = "失败"
    try:  # 发邮件
        server = smtplib.SMTP(smtp_server, 25)  # 连接发件人邮箱的SMTP服务器,端口是25
        if flag == "成功":
            msg = MIMEText(f'【TAPD定时邮件结果】{flag}!', 'plain', 'utf-8')
        else:
            msg = MIMEText(f'【TAPD定时邮件结果】{flag}!电脑可能已断网,请及时处理重新发送邮件!', 'plain', 'utf-8')
        # msg = MIMEText(f'【TAPD定时邮件结果】{flag}!请及时处理!', 'plain', 'utf-8')
        msg['From'] = formataddr((f"定时日报结果通知>{flag}", email_user))  # 括号里的对应发件人邮箱昵称、发件人邮箱账号
        msg['To'] = formataddr(("收件人", email_user))  # 括号里的对应收件人邮箱昵称、收件人邮箱账号
        msg['Subject'] = f"【TAPD定时邮件结果】{flag}!"  # 邮件主题
        server.login(email_user, email_passwd)  # 括号中对应的是发件人邮箱账号、邮箱密码
        server.sendmail(email_user, [email_user, ], msg.as_string())  # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
        # print('\n"定时器结果"通知成功,请检查邮箱:【{0}】收件箱访问'.format(email_user))
        server.quit()  # 关闭连接
    except Exception as err:
        print('发送"定时器结果通知"邮件失败:{0}\n如果网络正常,请确认邮件账号密码是否正确'.format(err))
        return False


# 更新、校验测试截至时间
def get_send_date(timer=False, set_end=0, start_time=None, end_time=None):
    """根据当前时间、是否启用定时器以及定时器定时时长得到邮件实际发送日期
    timer:bool
    set_end:int
    start_time:测试开始时间 yyyy-mm-dd
    end_time:测试结束日期 yyyy-mm-dd
    return: end_time
    """
    if timer:  # 启用了定时器
        time_now = int(set_end / 60) + int(datetime.datetime.now().strftime('%H')) + 1  # 计算真实的发送时间
    else:
        time_now = int(datetime.datetime.now().strftime('%H'))  # 当前时
    # time_now = int(datetime.datetime.now().strftime('%H'))  # 当前时
    today = datetime.date.today()  # 今天
    next_day = end_time + datetime.timedelta(days=int(1))  # 下一日
    next_workday = tommorow(1, end_time=end_time)  # 下一工作日
    last_workday = tommorow(-1, end_time=end_time)  # 上一工作日
    if int(time_now) < 24 and end_time == today:  # 今天发
        if is_workday(end_time):  # 今天是工作日
            if 6 <= int(time_now) < 12:  # 上午
                if sg.popup(f"日报发送时辰约为【上午{time_now}点】,可能暂无缺陷数据;\n"
                            f"是否更改【测试截止时间】为上一工作日\n取【{last_workday}】的数据发送日报?",
                            custom_text=(f'{last_workday}', '    NO    '),
                            title='确认') == f'{last_workday}':  # 中午前 # 工作日
                    end_time = last_workday  # 上一工作日
            elif int(time_now) < 6:  # 凌晨
                if sg.popup(f"日报发送时辰约为【凌晨{time_now}点】,可能暂无缺陷数据;\n"
                            f"是否更改【测试截止时间】为上一工作日\n取【{last_workday}】的数据发送日报?",
                            custom_text=(f'{last_workday}', '    NO    '),
                            title='确认') == f'{last_workday}':  # 中午前 # 工作日
                    end_time = last_workday  # 上一工作日

        else:  # 今天是休息日
            if sg.popup(f"今天为休息日,注意休息;\n是否更改【测试截止时间】为上一工作日\n取【{last_workday}】的数据发送日报?\n",
                        custom_text=(f'{last_workday}', '    NO    '),
                        title='确认') == f'{last_workday}':  # 休息日
                end_time = last_workday  # 上一工作日

    elif 24 <= int(time_now) <= 48 and end_time == today:  # 明天发
        if is_workday(next_day):  # 明天
            if sg.popup(f"日报发送日期预计为工作日:【{next_day}日{time_now % 24}点】;\n"
                        f"是否更改【测试截止时间】为下一工作日\n取【{next_workday}】的数据发送日报?\n",
                        custom_text=(f'{next_workday}', '    NO    '),
                        title='确认') == f'{next_workday}':  # 真实发送时间到了下一天的下午24+12 # 下一天是工作日
                end_time = next_workday  # 取下一工作日
        elif sg.popup(f"日报发送日期预计为【{next_day}】休息日;\n"
                      f"请选择其中一天作为【测试截止时间】\n将取其数据发送日报\n", custom_text=(f'今天[{str(end_time)}]', f'明天[{str(next_day)}]'),
                      title='确认') == f'明天[{str(next_day)}]':  # 真实发送时间到了下一天的下午24+12 # 下一天不是工作日
            end_time = next_day

    elif int(time_now) > 48 and end_time == today:  # 定时更久了,明天以后发
        daynum = time_now // 24 + 1
        send_time = end_time + datetime.timedelta(days=int(daynum))  # 发送日
        sg.popup(f"日报预计发送日期预计为{send_time}\n请选择起始日期{start_time}与预计发送日期{send_time}之间的日期\n作为【测试截止日期】", title='确认')
        Flag = True
        while Flag:
            date = sg.popup_get_date(close_when_chosen=True, month_names=month_names, start_day=15)
            text = str(date[2]) + '-' + str(date[0]) + '-' + str(date[1])
            end_time = datetime.date(*map(int, text.split('-')))
            if start_time <= end_time <= send_time:
                Flag = False
            else:
                sg.popup(f"只能选择起始日期{start_time}与预计发送日期{send_time}之间的日期\n作为【测试截止日期】\n请重新选择!", title='确认')

    if end_time >= start_time:  # 确保结束日期大于开始日期的前提下,更新结束时间为邮件实际发送日期
        if end_time != datetime.date.today():
            print(f"本次日报发送您选取了自【{start_time}】截至【{end_time}】的数据,请注意> > >\n")
        return str(end_time)
    else:
        sg.popup("测试结束日期不能早于测试开始日期", title='确认')
        return "测试结束日期不能早于测试开始日期"


# 得到一个应景诗词token
def _get_verse_token(verse_token):
    try:
        token_url = "https://v2.jinrishici.com/token"
        res = requests.get(url=token_url)
        assert res.status_code == 200
        token = res.json().get("data")
        if token:
            write_conf(path, "SET", 'verse_token', token)
        else:
            write_conf(path, "SET", 'verse_token')
        return token
    except Exception as error:
        # print("异常信息", error)
        return False


# 得到一句应景诗词
def _get_a_verse(token):
    try:
        verse_url = "https://v2.jinrishici.com/sentence"
        headers = {
            "X-User-Token": token,
            # "user-agent": Faker().chrome(version_to=95)
        }
        ress = requests.get(url=verse_url, headers=headers)
        status = ress.json().get("status")
        assert ress.status_code == 200
        if status == "success":
            data = ress.json().get("data").get("content")
            return data
        else:
            return False
    except Exception as error:
        # print("异常信息2", error)
        return False


# 判断token,返回诗一行
def get_verse(verse_token, flag=None, tag='forgo'):
    # flag: 为0返回一句诗,为1返回带格式提示语
    # forgo:放弃
    try:
        data = _get_a_verse(verse_token)  # 第一次获取诗句
        if not data:  # 您携带的Token不合法,请重新从指定接口签发  # 空/无效
            token = _get_verse_token(verse_token)
            time.sleep(1)
            data = _get_a_verse(token)  # 更新token,第二次获取诗句
            write_conf(path, "SET", 'verse_token', token)  # token写配置文件 # path是全局变量
        if data:
            verse = data
        else:
            verse = "中国企业,全球梦想"
    except:
        verse = "中国企业,全球梦想."
    if flag:
        num = int(abs((131 - 3.4375 * len(verse)) // 2 - 11))
        verse = '-' * num + f'***{tag}***' + '<  ' + verse + '>' + '-' * num
        return verse
    return verse


# 增加配置文件中配置项的方法
def add_config(path):
    # 专门用来增加配置文件中配置项的方法,增量增加,不影响老配置
    add_option(path, "SET", 'verse_token', "")  # 增加配置项:SET:verse_token  # 2021-11-26


# 彩蛋事件
def event_LifeEgg(event):

    if event == '【镇山的虎】':  # 游戏
        url_yx = ['https://cn.puzzle-loop.com/', 'https://cn.puzzle-sudoku.com/', 'https://cn.puzzle-binairo.com/',
                  'https://iogames.space/', 'https://www.yikm.net/', 'https://poki.com/', 'https://www.crazygames.com/']
        webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])

    if event == '【远见的鹰】':  # 小说、漫画、动漫
        url_yx = ['http://www.iewoai.com/', 'https://www.qianduanmei.com/', 'https://www.kgbook.com/',
                  'https://tel.dm5.com/',
                  'https://new.shuge.org/', 'http://www.silisili.in/', 'Http://www.nicotv.me/',
                  'Https://www.agefans.cc/',
                  'Https://www.kanman.com/', 'Http://www.dm5.com/']
        webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])

    if event == '【忠诚的狗】':  # 黑科技
        url_yx = ['https://essay.1ts.fun/', 'https://koutu.gaoding.com/', 'http://www.ixiqi.com/',
                  'https://www.67tool.com/',
                  'https://carrotchou.lanzoux.com/b0gwopzc',
                  'https://www.wechatsync.com/?utm_source=xinquji&utm_source=xinquji#install',
                  'https://www.ghxi.com/', 'https://lanzoui.com/ivBxCmg0xyf', 'https://bigjpg.com/']
        webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])

    if event == '【善战的狼】':  # IT测试相关
        url_yx = ['https://chrome.zzzmh.cn/#/index', 'https://www.toolfk.com/',
                  'https://smalltool.github.io/2020/06/10/soft106/',
                  'https://www.canva.cn/?display-com-option=true', 'https://testerhome.com/columns/sylan215',
                  'https://www.luogu.com.cn/']
        webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])

    if event == '【害群的马】':  # 官网
        url_yx = ['http://www.ztccloud.com.cn/']
        webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])

    if event == '【盛饭的桶】':  # 外卖
        url_yx = ['https://waimai.meituan.com/', 'https://www.ele.me/']
        webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])

    if event == '【卸载工具】':
        if sg.popup_yes_no('是否确认卸载此工具?', title='') == 'Yes':
            if os.path.exists(path_pic):
                try:
                    os.removedirs(path_pic)
                    sg.popup(f"清理完成\n请退出当前程序,手动删除【当前工具.exe】文件\n即可完成卸载!", title='')
                except:
                    sg.popup(f"删除失败\n可手动卸载:\n1、退出后删除当前工具.exe文件;\n2、删除此文件夹:\n{path_pic}", title='')
                    print(f"删除失败\n可手动卸载:\n1、退出后删除当前工具.exe文件;\n2、删除此文件夹:\n{path_pic}")
        else:
            print(get_verse(verse_token, flag=True, tag='forgo'))

    if event == '【搅屎的棍】':
        if sg.popup_yes_no('还打算继续加班吗?\n还卷吗?', title='') == 'Yes':
            sg.popup(f"啊朋友再见吧,再见吧,再见吧!", title='回头是岸')
            url_yx = ['http://csga.changsha.gov.cn/']
            webbrowser.open(url_yx[random.randint(0, len(url_yx)) - 1])
        else:
            sg.popup("          小小傻猪了不起,天天睡到十点起。              \n"
                     "          餐餐五碗才垫底,体重没有谁敢比。\n"
                     "          没心没肺厚脸皮,谁人看见都妒忌。\n"
                     "          要问小猪在哪里,正在划水看消息。\n", title="哈哈哈...")

    if event == '你的队友:' or event == '而你:':
        print(get_verse(verse_token))


if __name__ == '__main__':
    im = None
    tool_vision = "2022_5_31"
    today = datetime.datetime.today().strftime('%Y-%m-%d')  # 获取今日日期:str
    # 使用协程检查网络
    g = gevent.spawn(checkNet_process)
    g.join()
    # config = configparser.ConfigParser()  # 存在百分号问题,故放弃,使用基层读取方法RawConfigParser
    config = configparser.RawConfigParser()
    path_pic = rf'{os.getenv("APPDATA")}' + '\\tapdconf' + '\\'  # 应用文件路径
    path = path_pic + rf'tapdconf_{tool_vision}.ini'  # 配置文件路径
    path_milestone = path_pic + rf'里程碑截图.jpg'  # 里程碑截图存储路径
    TAPD_list = ['cookie', 'url_tapd', 'id', 'start_time', 'smtp_server', 'email_user', 'email_passwd', 'iteration_id',
                 'os1',
                 'release_id', 'testphase', 'mail_title', 'b2', 'b1', 'addressee', 'mail_cc', 'daily_text',
                 'milestone_address', 'milestone_excel', 'manpower_today', 'test_situation',
                 'tomorrow_plans', 'list_os', 'list_id', 'list_iteration', 'list_testphase', 'list_release_id',
                 'win2_flag']  # 顺序敏感

    a2 = read_conf(path, TAPD_list)  # 读取配置文件tapdconf.ini的数据
    layout = tool_ui(a2)  # 加载工具的布局
    window_name = f"TAPD BUG REPORTS"
    window = sg.Window(window_name, layout, finalize=True, enable_close_attempted_event=True)  # 加载布局生成窗口

    """增加配置项"""
    add_config(path)

    # verse_token校验
    verse_token = get_option(path, "SET", 'verse_token')  # 读verse_token
    if not get_verse(verse_token):  # verse_token不存在或者无效就重读verse_token
        verse_token = get_option(path, "SET", 'verse_token')

    flag_UpdateConf = False  # 是否更新配置文件的标志
    old_id = a2["id"]  # 获取界面初始id的值
    old_iteration_id = a2["iteration_id"]  # 获取界面初始迭代版本id的值
    old_release_id = a2["release_id"]  # 获取界面初始发布计划的值
    id_name = a2['list_id'].split('_toolflag_')  # 初始id列表
    iteration_update = a2['list_iteration'].split('_toolflag_')  # 初始迭代版本id列表
    release_update = a2['list_release_id'].split('_toolflag_')  # 初始发布计划列表
    # 预设TAPD缺陷显示字段
    status_tool = 'id;closed;title;reject_time;reopen_time;severity;priority;status;current_owner;reporter;created;fixer;resolved;de;closer;custom_field_four;'
    # 窗口二,适应自定义URL
    win2_active = False  # win2活跃标志
    window_active = False  # window活跃标志
    timer = False  # 是否定时标志
    set_end = 0  # 邮件定时(min)时长默认零
    while True:
        window_active = True  # window活跃标志
        event, value = window.read(300)
        for k, v in value.items():  # 值去空
            if isinstance(k, str):
                pass
            else:
                break
            if k in "cookie,id,start_time,smtp_server,email_user,email_passwd,iteration_id,title,os1,testphase,addressee,mail_cc":
                value[k] = v.strip()

        cookie = value['cookie'].replace("_toolflag_", "%")
        if event == '清空':
            # 点击清空按钮
            # 1、清空可能会变化的字段信息(没做)
            # 2、清空日志栏
            window['debug_result'].Update(value='')
            pass

        elif event == 'collapse_control':
            # 界面折叠功能,未实现,先把按钮注释了
            if window[event].get_text() == '︽':
                # 折叠
                window[event].Update(text='︾')
                window['frame1'].update(visible=False)
            else:
                window[event].Update(text='︾')
                window['frame1'].update(visible=True)

        elif (event == 'url_method' and not win2_active) or (a2["win2_flag"] == "Y"):  # win2退出的,直接进win2
            # 切换界面,另一种查询方式
            win2_active = True
            window.Hide()
            win2_flag = "Y"
            write_conf(path, "TAPD", 'win2_flag', win2_flag)
            # window.close()
            a2 = read_conf(path, TAPD_list)  # 读取配置文件tapdconf.ini的数据
            layout2 = tool_ui2(a2)  # 加载工具的布局
            win2 = sg.Window('CUSTOM REQUEST REPORTS', layout2, finalize=True, enable_close_attempted_event=True)
            while True:
                ev2, vals2 = win2.read(300)
                if ev2 == '执行':
                    cookie = vals2['cookie'].replace("_toolflag_", "%")
                    if len(cookie) == 0:
                        sg.popup("[TAPD_Cookie]不能为空", title='')
                        continue

                    url_tapd = vals2['url_tapd']
                    if len(url_tapd) == 0:
                        sg.popup("[获取数据URL]不能为空", title='提示')
                        continue

                    smtp_server = vals2['smtp_server']  # 默认为腾讯企业邮箱服务器地址,如果用的其他邮箱请更新smtp服务器地址
                    if len(smtp_server) == 0:
                        sg.popup("[邮件服务器地址]不能为空", title='提示')
                        continue

                    email_user = vals2['email_user']  # 邮件账号
                    if len(email_user) == 0:
                        sg.popup("[邮件服务器账号]不能为空", title='提示')
                        continue
                    if '@' not in email_user:
                        sg.popup("[邮件服务器账号]格式错误", title='提示')
                        continue

                    email_passwd = vals2['email_passwd']  # 邮件密码
                    if len(email_passwd) == 0:
                        sg.popup("[邮件服务器密码]不能为空", title='提示')
                        continue

                    b2 = '是' if str(vals2['b2']) == 'True' else ''  # 填"是"则包含节假日(节假日指非工作日)

                    # 邮件收件人
                    addressee = vals2['addressee'].replace(',', ',')
                    search_mail = re.findall('@[^,]*@', addressee)
                    search_comma = re.findall(',[^@]*,', addressee)
                    if addressee != '' and '@' not in addressee:
                        sg.popup("[邮件收件人]格式错误", title='提示')
                        continue
                    if len(search_mail) != 0 or len(search_comma) != 0:  # 判断@与,是否交替出现
                        sg.popup("[邮件收件人]校验错误!\n请确认每个邮箱之间都使用单个逗号隔开", title='提示')
                        continue
                    start_time = vals2["start_time"]  # 测试开始时间
                    if date_check(start_time):
                        continue

                    # 邮件抄送人
                    mail_cc = vals2['mail_cc'].replace(',', ',')
                    search_mail = re.findall('@[^,]*@', mail_cc)
                    search_comma = re.findall(',[^@]*,', mail_cc)
                    if mail_cc != '' and '@' not in mail_cc:
                        sg.popup("[邮件抄送人]格式错误", title='提示')
                        continue
                    if len(search_mail) != 0 or len(search_comma) != 0:  # 判断@与,是否交替出现
                        sg.popup("[邮件抄送人]校验错误!\n请确认每个邮箱之间都使用单个逗号隔开", title='提示')
                        continue

                    # 保存界面数据
                    save_data(vals2, a2, flag_UpdateConf)

                    # 判断邮件发给谁
                    if len(vals2['addressee']) != 0:  # 如收件人列表为空,则发给自己
                        list_addressee = addressee.split(',')  # 去空、去中文逗号,以英文逗号隔开处理成列表
                        list_addressee = list(set(list_addressee))  # list去重
                        if len(vals2['mail_cc']) != 0:  # 如抄送人有值
                            list_mail_cc = mail_cc.split(',')
                            list_mail_cc = list(set(list_mail_cc))  # list去重
                        else:
                            list_mail_cc = [vals2['email_user']]
                    else:
                        list_addressee = [vals2['email_user']]
                        list_mail_cc = [vals2['email_user']]
                        print("[邮件收件人]为空,此邮件将直接发给【%s】\n" % vals2["email_user"])

                    timer = win2['timer'].get()  # 是否定时发送邮件
                    set_end = int(float(vals2["timer_slider"]))  # 邮件定时(min)时长
                    start_time_int = datetime.date(*map(int, start_time.split("-")))
                    end_time = vals2["end_time"]  # 结束时间【可优化成配置】
                    end_time_int = None
                    Flag = True
                    error_times = 0
                    while Flag:
                        try:
                            end_time_int = datetime.date(*map(int, end_time.split("-")))
                            if end_time_int < start_time_int:
                                error_times = error_times + 1
                                sg.popup(f"【测试截止日期】不能小于【测试开始日期】\n请使用控件选择【测试截止日期】后\n重新执行", title='确认')
                                date = sg.popup_get_date(close_when_chosen=True, month_names=month_names, start_day=15)
                                end_time = str(date[2]) + '-' + str(date[0]) + '-' + str(date[1])
                                win2['end_time'].Update(value=end_time)  # 更新到界面
                            else:
                                Flag = False
                        except:
                            error_times = error_times + 1
                            sg.popup(f"【测试截止日期】格式填写错误\n请使用控件选择【测试截止日期】后\n重新执行", title='确认')
                            date = sg.popup_get_date(close_when_chosen=True, month_names=month_names, start_day=15)
                            end_time = str(date[2]) + '-' + str(date[0]) + '-' + str(date[1])
                            win2['end_time'].Update(value=end_time)  # 更新到界面
                    if error_times > 0:  # 有过错误,停下来
                        continue
                    end_time_int = datetime.date(*map(int, end_time.split("-")))

                    # 更新end_time为邮件实际发送日期 # 全局变量  # g/et_date()、analyze_datas()、send_mail()、end_time
                    list_date = []
                    list_date_Source = "chinese_calendar"  # 时间轴日期列表取值方式,默认chinese_calendar库取 # 页面全局变量
                    try:  # 检查chinese_calendar库是否适用
                        is_workday(end_time_int)
                    except NotImplementedError:
                        # sg.popup("chinese_calendar库太旧,下面自动更新chinese_calendar库", title='提示')
                        # os.system("pip install --disable-pip-version-check --upgrade chinese_calendar")
                        list_date_Source = "by_hand"  # 已不可能chinese_calendar
                        str_date = sg.popup_get_text(f"测试时间区间跨年了,chinese_calendar库不能自动获取到所需年份对应的版本.\n所以只能手动处理\n"
                                                     f"{'-' * 55}解决方式1{'-' * 55}"
                                                     "\n手动输入【测试起止日期】区间内的所有你需要统计数据的日期到下面输入框\n格式为[yyyy-mm-dd],次序为[从早到晚],日期间[逗号隔开]\n"
                                                     "工具将取你填写的第一个日期作为【测试开始时间】,最后一个日期为【测试结束时间】\n完成后点击Ok."
                                                     f"\n{'-' * 55}解决方式2{'-' * 55}"
                                                     "\n点击Cancel或者右上角X退出,联系管理员重新打包.\n", title='提示:遇上麻烦事了!!!请仔细阅读',
                                                     size=(70, 1))
                        if str_date:  # 有值
                            list_date = str_date.replace(",", ",").split(',')
                            # 列表去重、去空 # 校验各项日期格式
                            if len(list_date):
                                list_date2 = list_date
                                list_date = list(set(list_date))  # 去重
                                list_date.sort(key=list_date2.index)
                                r = re.compile('^\d{4}-\d{2}-\d{2}')  # 正则规则,yyyy-mm-dd
                                list_date = [i for i in list_date if i != '' and r.match(i)]  # 去空, 去非日期格式
                            if len(list_date):
                                if sg.popup_yes_no(f"您手动填写的日期列表整理后为:\n{','.join(list_date)}\n{'-' * 60}\n1、日期"
                                                   f"(除休息日)如不连续影响画图\n2、请确认列表中日期格式都正确\n3、日期列表与URL中的"
                                                   f"日期范围需一致\n{'-' * 60}\n错误的话点击[No]中止发送",
                                                   title='确认') == "No":
                                    print(f"手动填写的日期列表为\n{','.join(list_date)}\n已中止发送,可以修改正确后复制粘贴再次尝试")
                                    continue  # 中止
                                # 继续
                            else:
                                sg.popup(f"您手动填写的日期列表格式完全不正确\n请重新操作", title='提示')
                                continue
                        else:
                            print(get_verse(verse_token, flag=1, tag='forgo'))  # 中止提示
                            continue
                    except:
                        sg.popup("测试时间区间跨年了,chinese_calendar库不适用当前年份,\n请联系管理员重新打包", title='提示')
                        continue
                    if list_date_Source == "chinese_calendar":
                        end_time = get_send_date(timer, set_end, start_time_int, end_time_int)  # 更新结束时间
                        if end_time == "测试结束日期不能早于测试开始日期":
                            continue
                    else:
                        if len(list_date) > 0:
                            start_time = list_date[0]
                            end_time = list_date[-1]
                            print(f"本次日报发送您选取了【{','.join(list_date)}】作为日期数据,如果日期(除休息日)不连续数据会不准确,请注意> > >\n")
                    try:  # 生成时间轴
                        if list_date_Source == "chinese_calendar":
                            list_date = get_date(start_time, end_time, b2)  # list_date:趋势图时间轴
                    except NotImplementedError:
                        sg.popup("chinese_calendar库不适用当前年份,\n请联系管理员", title='提示')
                        continue
                    # if data_tapd is False:
                    #     sg.popup("[获取数据URL]不可用,\n请确保此URL是复制的触发了TAPD过滤器的URL。\n若有疑问请联系管理员", title='提示')
                    #     continue
                    try:
                        view_id, conf_id, id, bugs_numb = get_bugs_list_by_url(cookie, url_tapd, page=1, perpage=100,
                                                                               flag="info")  # wqflag
                    except:
                        continue
                    """获取现有TAPD缺陷显示字段"""
                    status_old = get_show_fields(cookie, id, view_id)
                    if status_old is False:
                        continue
                    """获取BUG总数"""
                    print("\n----------------------------------------   执行开始[",
                          time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                          "]  ---------------------------------------\n")

                    if bugs_numb == 0:
                        button_return = sg.popup_ok_cancel("项目下当前迭代版本缺陷总数为0,建议先确认。\n是否继续发送邮件?")
                        if button_return == 'Cancel':
                            print(f"当前迭代版本缺陷总数为0,中止执行.<{get_verse(verse_token)}>")
                            print("\n----------------------------------------   中止执行[",
                                  time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                                  "]  ---------------------------------------\n")
                            continue

                    try:
                        """设置TAPD缺陷显示字段为预设字段"""
                        if edit_show_fields(cookie, id, view_id, status_tool) is False:
                            continue

                        """得到所有缺陷的字段数据,并记录成插表语句"""
                        list_sql = []  # 记录所有缺陷数据处理后得到的插表sql
                        if bugs_numb > 200:
                            print(f"一共【{bugs_numb}】条缺陷,生成图片需要一定时间,请稍等\n")
                        else:
                            print(f"一共【{bugs_numb}】条缺陷:\n")
                        for i in range(1, math.ceil(bugs_numb / 100) + 1):
                            data_tapd = get_bugs_list_by_url(cookie, url_tapd, page=i, perpage=100, flag="bugs_list")
                            if data_tapd is False:
                                continue
                            list_sql = list_sql + data_cleanup(data_tapd)
                        if len(list_sql) == 0 and bugs_numb != 0:
                            sg.popup("解析缺陷数据失败!\n请确认[获取数据URL]是不是基于【缺陷视图-所有的】而创建!\n如果一直无法解决可以切换成工具的日常模式使用。\n若有疑问请联系管理员~",
                                     title='提示')
                            continue
                        """处理里程碑输入框:读取里程碑输入框内容,如果是excel粘贴的,则处理成html中的table标签"""
                        list_milestone_excel = vals2['milestone_excel'].replace("_toolflag_", "	").split('\n')
                        if len(list_milestone_excel) > 5:  # 判断内容5行以上,当作是里程碑表格处理格式
                            table_text = text_to_table(list_milestone_excel, vals2['milestone_excel'])
                        else:
                            table_text = vals2['milestone_excel']

                        list_vals2 = [vals2['daily_text'], vals2['test_situation'], vals2['tomorrow_plans'],
                                      vals2['manpower_today'],
                                      vals2['milestone_address'], table_text, vals2['mail_title']]

                        """整理数据,发邮件"""
                        send_mail(email_user, email_passwd, list_addressee, list_mail_cc, smtp_server,
                                  analyze_datas(list_sql, list_date, id), list_vals2)

                    except Exception as err:
                        if isinstance(err, UnicodeEncodeError):
                            print("\n邮件发送失败! [邮件服务器账号/密码/收件人/抄送人]输入了异常字符,比如中文。\n错误信息:", err)
                        else:
                            print("处理缺陷数据出错,请联系管理员。\n[1]错误信息:", err)
                        continue

                    finally:
                        """还原TAPD缺陷显示字段"""
                        # visible_feild(status_old, id, cookie)

                        if edit_show_fields(cookie, id, view_id, status_old) is False:
                            pass

                        print("\n----------------------------------------   执行结束[",
                              time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                              "]  ---------------------------------------\n")

                elif ev2 == '暂存':
                    # 点击暂存按钮
                    # 将界面上填写的内容存到配置文件
                    # 更新配置内容
                    save_data(vals2, a2, flag_UpdateConf)

                elif ev2 == '贴图':
                    # 点击贴图按钮
                    # 1、读取剪切板截图内容,保存成图片
                    # 2、插入邮件模板的里程碑进度图的位置
                    im = ImageGrab.grabclipboard()
                    try:
                        im.save(path_milestone)
                        print("【里程碑进度截图】插入成功\n")
                    except Exception as error:
                        # 再操作说明想改,删除历史图片
                        if os.path.exists(path_milestone):
                            os.remove(path_milestone)
                        sg.popup("该操作原理是读取剪切板\n请先【截取里程碑进度】的图片再操作", title='错误')

                elif ev2 == '预览':
                    # 点击预览按钮
                    # 1、预览里程碑截图
                    show_figure(path_milestone)


                elif ev2 == 'url_method':
                    win2.hide()
                    win2_active = False
                    win2_flag = "N"
                    write_conf(path, "TAPD", 'win2_flag', win2_flag)
                    # window.close()
                    a2 = read_conf(path, TAPD_list)  # 读取配置文件tapdconf.ini的数据
                    layout = tool_ui(a2)  # 加载工具的布局
                    window = sg.Window(window_name, layout, finalize=True,
                                       enable_close_attempted_event=True)  # 加载布局生成窗口
                    break

                elif ev2 == '源码':
                    if sg.popup_yes_no("是否打开存放源码的博客?", title='确认') == 'Yes':
                        url = 'https://blog.csdn.net/cat_l_over/article/details/120185016'
                        webbrowser.open(url)

                elif (ev2 in ['【镇山的虎】', '【远见的鹰】', '【忠诚的狗】', '【善战的狼】', '【害群的马】', '【盛饭的桶】', '【卸载工具】', '【搅屎的棍】', '你的队友:',
                              '而你:']):
                    event_LifeEgg(ev2)

                elif (ev2 == sg.WINDOW_CLOSE_ATTEMPTED_EVENT or ev2 == sg.WINDOW_CLOSED) and \
                        sg.popup_yes_no('退出程序?', title='确认') == 'Yes':
                    window_active = False
                    win2_active = False
                    win2_flag = "Y"
                    write_conf(path, "TAPD", 'win2_flag', win2_flag)
                    break
            win2.close()
            if window_active is False:
                break

        elif event == '暂存':
            # 点击暂存按钮
            # 将界面上填写的内容存到配置文件
            # 更新配置内容
            save_data(value, a2, flag_UpdateConf)

        elif event == '贴图':
            # 点击贴图按钮
            # 1、读取剪切板截图内容,保存成图片
            # 2、插入邮件模板的里程碑进度图的位置
            im = ImageGrab.grabclipboard()
            try:
                im.save(path_milestone)
                print("【里程碑进度截图】插入成功\n")
            except Exception as error:
                # 再操作说明想改,删除历史图片
                if os.path.exists(path_milestone):
                    os.remove(path_milestone)
                sg.popup("该操作原理是读取剪切板\n请先【截取里程碑进度】的图片再操作", title='错误')

        elif event == '预览':
            # 点击预览按钮
            # 1、预览里程碑截图
            show_figure(path_milestone)

        elif event == 'GET':
            # 点击GET按钮
            # 1、根据cookie更新项目list
            # 2、list写入配置文件(配置文件需要增加存储动态下拉框的类型[LIST])
            if len(cookie) == 0:
                sg.popup("[TAPD_Cookie]不能为空", title='')
                continue
            else:
                id_name = tapd_id(cookie)  # 获取项目id列表
                if id_name:
                    window['id'].Update(values=id_name)
                    write_conf(path, "TAPD", 'list_id', '_toolflag_'.join(id_name))
                else:
                    sg.popup("获取项目列表为空,可选择手动输入[项目]id", title='提示')
                    continue

        elif event == 'id':
            # 界面触发id列表选择事件,立刻恢复下拉列表的内容完整
            # 刷新获取迭代版本
            # 跳过项目id字段更新事件
            old_id = value['id']  # 触发了选择事件,就让更新的判断失效,以不再进更新
            window['id'].Update(value=value["id"], values=id_name)
            if len(cookie) == 0:
                sg.popup("[TAPD_Cookie]不能为空", title='')
                continue
            # 迭代版本id
            try:
                iteration_update = get_options(cookie, value['id'][-8:])  # wqflag
                if len(iteration_update) > 0:
                    window['iteration_id'].update(values=iteration_update)
                    print("成功获取所选项目下的[迭代版本]\n")
                else:
                    print("获取当前项目[迭代版本]失败,请检查[TAPD_Cookie]、[项目]字段,或手动填写迭代版本id")
            except Exception as err:
                if loglevel == "debug":
                    raise err
                sg.popup("使用[项目]更新[迭代版本]失败!\n请检查[TAPD_Cookie]与[项目]是否匹配", title='提示')
                continue
            # 发布计划id
            try:
                release_update = get_release_id(cookie, value['id'][-8:])  # 带有发布计划信息的html文件
                if release_update is False:
                    continue
                if len(release_update) > 0:
                    window['release_id'].update(values=release_update)
                    print("成功获取所选项目下的[发布计划]\n")
                    # 预备写配置文件
                    config.read(path)
                    iteration_update_ = unicodedata.normalize('NFKC', '_toolflag_'.join(iteration_update))
                    release_id_update_ = unicodedata.normalize('NFKC', '_toolflag_'.join(release_update))
                    config.set("TAPD", 'list_iteration', iteration_update_)  # 迭代版本更新至配置文件
                    config.set("TAPD", 'list_release_id', release_id_update_)  # 发布计划更新至配置文件
                    flag_UpdateConf = True
                else:
                    print("获取当前项目[发布计划]为0,如异常请检查[TAPD_Cookie]、[项目]字段,或手动填写发布计划id")
            except Exception as err:
                if loglevel == "debug":
                    raise err
                continue


        elif value['id'] != old_id:
            # 根据项目id字段输入框的内容实时筛选项目下拉列表
            old_id = value["id"]
            window['id'].Update(value=value["id"],
                                values=search_result(id_name, value['id']))

        elif event == 'iteration_id':
            # 界面触发迭代版本id选择事件,还原迭代版本下拉列表内容
            window['iteration_id'].Update(value=value["iteration_id"],
                                          values=iteration_update)
            old_iteration_id = value['iteration_id']  # 触发了选择事件,就让更新事件失效,不再进更新

        elif value['iteration_id'] != old_iteration_id:
            # 根据迭代版本id字段输入框的内容实时筛选迭代版本下拉列表
            old_iteration_id = value["iteration_id"]
            window['iteration_id'].Update(value=value["iteration_id"],
                                          values=search_result(iteration_update, value['iteration_id']))

        elif event == 'release_id':
            # 界面触发迭代版本id选择事件,还原迭代版本下拉列表内容
            window['release_id'].Update(value=value["release_id"], values=release_update)
            old_release_id = value['release_id']  # 触发了选择事件,就让更新事件失效,不再进更新

        elif value['release_id'] != old_release_id:
            # 根据发布计划id字段输入框的内容实时筛选迭代版本下拉列表
            old_release_id = value["release_id"]
            window['release_id'].Update(value=value["release_id"],
                                        values=search_result(release_update, value['release_id']))

        elif event == '执行':
            # 获取界面信息
            if len(cookie) == 0:
                sg.popup("[TAPD_Cookie]不能为空", title='')
                continue

            id = value['id'].split('--')[-1]  # 项目ID
            if input_check(id):  # 检查项目id字段格式
                sg.popup("[项目]字段校验错误!\n如果是手动输入只支持输入id,请确认", title='提示')
                continue

            start_time = value["start_time"]  # 测试开始时间
            if date_check(start_time):
                continue

            # 以下验证必填项
            smtp_server = value['smtp_server']  # 默认为腾讯企业邮箱服务器地址,如果用的其他邮箱请更新smtp服务器地址
            if len(smtp_server) == 0:
                sg.popup("[邮件服务器地址]不能为空", title='提示')
                continue

            email_user = value['email_user']  # 邮件账号
            if len(email_user) == 0:
                sg.popup("[邮件服务器账号]不能为空", title='提示')
                continue
            if '@' not in email_user:
                sg.popup("[邮件服务器账号]格式错误", title='提示')
                continue

            email_passwd = value['email_passwd']  # 邮件密码
            if len(email_passwd) == 0:
                sg.popup("[邮件服务器密码]不能为空", title='提示')
                continue
            print("\n----------------------------------------   执行开始[",
                  time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                  "]  ---------------------------------------\n")
            # 更新配置内容
            save_data(value, a2, flag_UpdateConf)

            """以下过滤规则非必填"""
            # 邮件收件人
            addressee = value['addressee'].replace(',', ',')
            search_mail = re.findall('@[^,]*@', addressee)
            search_comma = re.findall(',[^@]*,', addressee)
            if addressee != '' and '@' not in addressee:
                sg.popup("[邮件收件人]格式错误", title='提示')
                continue
            if len(search_mail) != 0 or len(search_comma) != 0:  # 判断@与,是否交替出现
                sg.popup("[邮件收件人]校验错误!\n请确认每个邮箱之间都使用单个逗号隔开", title='提示')
                continue

            # 邮件抄送人
            mail_cc = value['mail_cc'].replace(',', ',')
            search_mail = re.findall('@[^,]*@', mail_cc)
            search_comma = re.findall(',[^@]*,', mail_cc)
            if mail_cc != '' and '@' not in mail_cc:
                sg.popup("[邮件抄送人]格式错误", title='提示')
                continue
            if len(search_mail) != 0 or len(search_comma) != 0:  # 判断@与,是否交替出现
                sg.popup("[邮件抄送人]校验错误!\n请确认每个邮箱之间都使用单个逗号隔开", title='提示')
                continue

            # 发布计划
            release_id = value['release_id'].split('--')[-1]
            if release_id != '':
                if input_check(release_id):  # 检查发布计划字段格式
                    sg.popup("[发布计划]字段校验错误!\n如果是手动输入只支持输入发布计划的id,请确认", title='提示')
                    continue

            # 迭代版本 (必须与项目id匹配),示例:2.1.7.2版本-收入结算平台-20210910
            iteration_id = value['iteration_id'].split('--')[-1]
            if iteration_id != '':
                if input_check(iteration_id):  # 检查发布计划字段格式
                    sg.popup("[迭代版本]字段校验错误!\n如果是手动输入只支持输入迭代版本的id,请确认", title='提示')
                    continue

            if iteration_id == '' and release_id == '':  # 都为空会查出大数据量缺陷,做出提示
                if sg.popup_yes_no("[迭代版本]、[发布计划]均为空\n将查询出项目下所有分支的缺陷\n是否继续?", title='确认') == 'No':
                    print("\n----------------------------------------   中止执行[",
                          time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                          "]  ---------------------------------------\n")
                    continue

            os1 = value['os1']  # 测试环境/UAT环境或者其他什么环境
            # title = value['title']  # 标题过滤
            title = ''  # 标题过滤
            testphase = value['testphase']  # 测试阶段
            b2 = '是' if str(value['b2']) == 'True' else ''  # 填"是"则包含节假日(节假日指非工作日)

            # 判断邮件发给谁
            if len(value['addressee']) != 0:  # 如收件人列表为空,则发给自己
                list_addressee = addressee.split(',')  # 去空、去中文逗号,以英文逗号隔开处理成列表
                list_addressee = list(set(list_addressee))  # list去重
                if len(value['mail_cc']) != 0:  # 如抄送人有值
                    list_mail_cc = mail_cc.split(',')
                    list_mail_cc = list(set(list_mail_cc))  # list去重
                else:
                    list_mail_cc = [value['email_user']]
            else:
                list_addressee = [value['email_user']]
                list_mail_cc = [value['email_user']]
                print("[邮件收件人]为空,此邮件将直接发给【%s】\n" % value["email_user"])

            timer = window['timer'].get()  # 是否定时发送邮件
            set_end = int(float(value["timer_slider"]))  # 邮件定时(min)时长
            start_time_int = datetime.date(*map(int, start_time.split("-")))
            # start_time = value["start_time"]  # 测试开始时间
            end_time = value["end_time"]  # 结束时间【可优化成配置】
            end_time_int = None
            Flag = True
            error_times = 0
            while Flag:
                try:
                    end_time_int = datetime.date(*map(int, end_time.split("-")))
                    if end_time_int < start_time_int:
                        error_times = error_times + 1
                        sg.popup(f"【测试截止日期】不能小于【测试开始日期】\n请使用控件选择【测试截止日期】后\n重新执行", title='确认')
                        date = sg.popup_get_date(close_when_chosen=True, month_names=month_names, start_day=15)
                        end_time = str(date[2]) + '-' + str(date[0]) + '-' + str(date[1])
                        window['end_time'].update(value=end_time)  # 更新到界面
                    else:
                        Flag = False
                except:
                    error_times = error_times + 1
                    sg.popup(f"【测试截止日期】格式填写错误\n请使用控件选择【测试截止日期】后\n重新执行", title='确认')
                    date = sg.popup_get_date(close_when_chosen=True, month_names=month_names, start_day=15)
                    end_time = str(date[2]) + '-' + str(date[0]) + '-' + str(date[1])
                    window['end_time'].update(value=end_time)  # 更新到界面
            if error_times > 0:  # 有过错误,停下来
                continue
            end_time_int = datetime.date(*map(int, end_time.split("-")))

            # 更新end_time为邮件实际发送日期 # 全局变量  # g/et_date()、analyze_datas()、send_mail()、end_time
            list_date = []
            list_date_Source = "chinese_calendar"  # 时间轴日期列表取值方式,默认chinese_calendar库取 # 页面全局变量
            try:  # 检查chinese_calendar库是否适用
                is_workday(end_time_int)
            except NotImplementedError:
                # sg.popup("chinese_calendar库太旧,下面自动更新chinese_calendar库", title='提示')
                # os.system("pip install --disable-pip-version-check --upgrade chinese_calendar")
                list_date_Source = "by_hand"  # 已不可能chinese_calendar
                str_date = sg.popup_get_text(f"测试时间区间跨年了,chinese_calendar库不能自动获取到所需年份对应的版本.\n所以只能手动处理\n"
                                             f"{'-' * 55}解决方式1{'-' * 55}"
                                             "\n手动输入【测试起止日期】区间内的所有你需要统计数据的日期到下面输入框\n格式为[yyyy-mm-dd],次序为[从早到晚],日期间[逗号隔开]\n"
                                             "工具将取你填写的第一个日期作为【测试开始时间】,最后一个日期为【测试结束时间】\n完成后点击Ok."
                                             f"\n{'-' * 55}解决方式2{'-' * 55}"
                                             "\n点击Cancel或者右上角X退出,联系管理员重新打包.\n", title='提示:遇上麻烦事了!!!请仔细阅读', size=(70, 1))
                if str_date:  # 有值
                    list_date = str_date.replace(",", ",").split(',')
                    # 列表去重、去空 # 校验各项日期格式
                    if len(list_date):
                        list_date2 = list_date
                        list_date = list(set(list_date))  # 去重
                        list_date.sort(key=list_date2.index)
                        r = re.compile('^\d{4}-\d{2}-\d{2}')  # 正则规则,yyyy-mm-dd
                        list_date = [i for i in list_date if i != '' and r.match(i)]  # 去空, 去非日期格式
                    if len(list_date):  # 去重后再次判断长度
                        if sg.popup_yes_no(
                                f"您手动填写的日期列表整理后为:\n{','.join(list_date)}\n{'-' * 60}\n1、日期(除休息日)如不连续影响画图\n2、请确认列表中日期格式都正确\n3、日期列表与URL中的日期范围需一致\n{'-' * 60}\n错误的话点击[No]中止发送",
                                title='确认') == "No":
                            print(f"手动填写的日期列表为\n{','.join(list_date)}\n已中止发送,可以修改正确后复制粘贴再次尝试")
                            continue  # 中止
                        # 继续
                    else:
                        sg.popup(f"您手动填写的日期列表格式完全不正确\n请重新操作", title='提示')
                        continue
                else:
                    print(get_verse(verse_token, flag=1, tag='forgo'))  # 中止发送
                    continue
            except:
                sg.popup("测试时间区间跨年了,chinese_calendar库不适用当前年份,\n请联系管理员重新打包", title='提示')
                continue
            if list_date_Source == "chinese_calendar":
                end_time = get_send_date(timer, set_end, start_time_int, end_time_int)  # 更新结束时间
                if end_time == "测试结束日期不能早于测试开始日期":
                    continue
            else:
                if len(list_date) > 0:
                    start_time = list_date[0]
                    end_time = list_date[-1]
                    print(f"本次日报发送您选取了【{','.join(list_date)}】作为日期数据,如果日期(除休息日)不连续数据会不准确,请注意> > >\n")

            try:  # 生成时间轴
                if list_date_Source == "chinese_calendar":
                    list_date = get_date(start_time, end_time, b2)  # list_date:趋势图时间轴
            except NotImplementedError:
                sg.popup("chinese_calendar库不适用当前年份,\n请联系管理员", title='提示')
                continue
            # data_tapd = get_datas(cookie, id, title, start_time, iteration_id, end_time, testphase, release_id, os1)  # 根据迭代版本id请求一次,获取现有TAPD缺陷显示字段以及当前迭代版本BUG总数
            view_id, conf_id = get_bug_fields_userview_and_list(cookie, id)  # wqflag

            # status_old = f'{";".join(re.findall(r"data-sort=.(.*?);", data_tapd, re.S))};'  # 获取现有TAPD缺陷显示字段
            status_old = get_show_fields(cookie, id, view_id)  # 获取现有TAPD缺陷显示字段
            if status_old is False:
                continue
            bugs_numb = int(
                get_bugs_list(cookie, id, conf_id, start_time, end_time, title, iteration_id, os1, testphase,
                              release_id, flag="total_count"))  # 获取BUG总数
            if bugs_numb is False:
                continue
            if bugs_numb == 0:
                button_return = sg.popup_ok_cancel("项目下当前迭代版本缺陷总数为0,建议先确认。\n是否继续发送邮件?")
                if button_return == 'Cancel':
                    print("当前迭代版本缺陷总数为0,中止执行")
                    print("\n----------------------------------------   中止执行[",
                          time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                          "]  ---------------------------------------\n")
                    continue

            continue_flag = True  # 继续发送邮件的标记
            if timer:  # 勾上了,定时是在取数据之前
                print(f'电脑自动睡眠/睡眠会导致断网,请设置电脑,保证倒计时期间电脑不会进入睡眠模式!')
                print(f'\n定时执行,倒计时:{set_end}分钟...')
                continue_flag = False
                i = 0  #
                sg.ChangeLookAndFeel('Black')  # #64778D
                sg.SetOptions(element_padding=(0, 0))
                layout3 = timer_ui()
                win3 = sg.Window('邮件发送倒计时', layout3, no_titlebar=False, auto_size_buttons=False, keep_on_top=True,
                                 grab_anywhere=True)
                paused = False  # 执行标志
                start_time = 0  # 记录一次执行开始的时间,s
                running_time = 0  # 记录一次执行时间 s
                left_time = set_end * 60  # 记录剩余时间 s
                show_time = left_time  # 最终用于展示的时间 s

                while True:
                    if paused:
                        ev3, vals3 = win3.read(1000)
                        now_time = int(time.time())  # 当前时间,s
                        running_time = now_time - start_time  # 执行时间,s
                        show_time = left_time - running_time  # 剩余时间
                    else:
                        ev3, vals3 = win3.read(1000)

                    button_name = win3["开始计时"].get_text()
                    if ev3 == '重新计时':
                        print(f'[{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}]重新计时,倒计时:{set_end}分钟')
                        # 变量统统复位
                        running_time = 0
                        left_time = set_end * 60
                        show_time = left_time
                        paused = False
                        win3['开始计时'].update(text='开始计时')
                        if window_active is False:
                            window.UnHide()
                            window_active = True
                            i = 0

                    if ev3 == '开始计时' and button_name == '暂停计时':  # 暂停
                        print(f'[{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}]暂停计时')
                        paused = False
                        start_time = int(time.time())
                        win3[ev3].update(text='开始计时')
                        if window_active is False:
                            window.UnHide()
                            window_active = True
                            i = 0
                    if ev3 == '开始计时' and button_name == '开始计时':
                        if i == 0:
                            print(f'[{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}]开始计时')
                            window.Hide()
                            window_active = False
                            i = 1
                        start_time = int(time.time())  # 开始时间,s
                        left_time = left_time - running_time
                        paused = True
                        win3[ev3].update(text='暂停计时')
                    if ev3 is None or ev3 == '中止发送':  # 中止发送
                        paused = False
                        continue_flag = False
                        break
                    if show_time <= 0 and button_name == '暂停计时':
                        paused = False
                        print(f'[{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}]计时结束\n')
                        win3["开始计时"].update(text='开始计时')
                        continue_flag = True
                        break

                    # --------- Display timer in win3 --------
                    win3['text_time'].update(
                        '{:02d}:{:02d}:{:02d}'.format(show_time // 3600, (show_time // 60) % 60, show_time % 60))
                win3.close()
                sg.ChangeLookAndFeel('BrownBlue')
                if window_active is False:
                    window_active = True
                    window.UnHide()
            if continue_flag is False:
                print("\n----------------------------------------   中止执行[",
                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                      "]  ---------------------------------------\n")
                continue

            try:
                """设置TAPD缺陷显示字段为预设字段"""
                visible_feild_tmp = edit_show_fields(cookie, id, view_id, status_tool)
                if visible_feild_tmp is False:
                    continue

                """得到所有缺陷的字段数据,并记录成插表语句"""
                list_sql = []
                if bugs_numb > 200:
                    print(f"一共【{bugs_numb}】条缺陷,生成图片需要一定时间,请稍等\n")
                else:
                    print(f"一共【{bugs_numb}】条缺陷:\n")
                for i in range(1, math.ceil(bugs_numb / 100) + 1):
                    data_tapd = get_bugs_list(cookie, id, conf_id, start_time, end_time, title, iteration_id, os1,
                                              testphase,
                                              release_id, page=i, perpage=100)
                    if data_tapd is False:
                        continue
                    list_sql = list_sql + data_cleanup(data_tapd)

                """读取里程碑输入框内容,如果是excel粘贴的,则处理成html中的table"""
                list_milestone_excel = value['milestone_excel'].replace("_toolflag_", "	").split('\n')
                if len(list_milestone_excel) > 5:  # 设置判断内容有5行以上才当作是里程碑表格处理格式
                    table_text = text_to_table(list_milestone_excel, value['milestone_excel'])
                else:
                    table_text = value['milestone_excel']
                list_value = [value['daily_text'], value['test_situation'], value['tomorrow_plans'],
                              value['manpower_today'],
                              value['milestone_address'], table_text, value['mail_title']]

                result_tool = send_mail(email_user, email_passwd, list_addressee, list_mail_cc, smtp_server,
                                        analyze_datas(list_sql, list_date, id), list_value)
                if result_tool and timer:
                    timer_result_mail(smtp_server, email_user, email_passwd, 0)  # 定时发送成功,发个通知邮件
            except Exception as err:
                if loglevel == "debug":
                    raise err
                if isinstance(err, UnicodeEncodeError):
                    print("\n邮件发送失败! [邮件服务器账号/密码/收件人/抄送人]输入了异常字符,比如中文。\n错误信息:", err)
                else:
                    print("处理缺陷数据出错,请联系管理员。\n[2]错误信息:", err)
                continue
            finally:
                """还原TAPD缺陷显示字段"""
                edit_show_fields(cookie, id, view_id, status_old)

                print("\n----------------------------------------   执行结束[",
                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                      "]  ---------------------------------------\n")
            # break

        elif event == '源码':
            if sg.popup_yes_no("是否打开存放源码的博客?", title='确认') == 'Yes':
                url = 'https://blog.csdn.net/cat_l_over/article/details/120185016'
                webbrowser.open(url)
            else:
                print(f"{get_verse(verse_token, flag=1, tag='forgo')}")
                continue

        elif (event in ['【镇山的虎】', '【远见的鹰】', '【忠诚的狗】', '【善战的狼】', '【害群的马】', '【盛饭的桶】', '【卸载工具】', '【搅屎的棍】', '你的队友:',
                        '而你:']):
            event_LifeEgg(event)

        elif (event == sg.WINDOW_CLOSE_ATTEMPTED_EVENT or event == 'Exit') and sg.popup_yes_no('退出程序?',
                                                                                               title='确认') == 'Yes':
            win2_flag = "N"
            write_conf(path, "TAPD", 'win2_flag', win2_flag)
            break
    window.close()

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值