1、邮箱服务器使用了腾讯服务器
具体操作见:python自动发邮件
2、变化的部分
3、上代码:
{# 引用模板 #} {% extends 'base.html' %} {% load staticfiles %} {% load comment_tags %} {% load likes_tags %} {% block header_extends %} <link rel="stylesheet" href="{% static 'blog/blog.css' %}"> <link rel="stylesheet" href="{% static 'fontawesome-free-5.5.0-web/css/all.min.css' %}"> {# 处理公式 #} <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script> <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script> <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script> {% endblock %} {# 标题 #} {% block title %} {{ blog.title }} {% endblock %} {# 内容#} {% block content %} <div class="container"> <div class="row"> <div class="col-10 offset-1"> <ul class="blog-info-description"> <h3>{{ blog.title }}</h3> <li>作者:{{ blog.author }}</li> {# 时间过滤器让时间按照自己需要的格式过滤 #} <li>发布日期:{{ blog.created_time|date:"Y-m-d H:i:s" }}</li> <li>分类: <a href="{% url 'blogs_with_type' blog.blog_type.pk %}"> {{ blog.blog_type }} </a> </li> <li>阅读({{ blog.get_read_num }})</li> <li>评论({% get_comment_count blog %})</li> </ul> <div class="blog-content">{{ blog.content|safe }}</div> <div class="like" onclick="likeChange(this,'{% get_content_type blog %}',{{ blog.pk }})"> <i class="far fa-thumbs-up {% get_like_status blog %}"></i> <span class="liked-num">{% get_like_count blog %}</span> <span>喜欢</span> </div> <p>上一篇: {% if previous_blog %} <a href="{% url 'blog_detail' previous_blog.pk %}">{{ previous_blog.title }}</a> {% else %} <span>没有了</span> {% endif %} </p> <p>下一篇: {% if next_blog %} <a href="{% url 'blog_detail' next_blog.pk %}">{{ next_blog.title }}</a> {% else %} <span>没有了</span> {% endif %} </p> </div> </div> <div class="row"> <div class="col-10 offset-1"> <div class="comment-area"> <h3 class="comment-area-title">提交评论</h3> {% if user.is_authenticated %} <form id="comment-form" action="{% url 'update_comment' %}" method="post" style="overflow: hidden"> {% csrf_token %} <label for="form-control">{{ user.get_nickname_or_username }},欢迎评论~</label> <div id="reply-content-container" style="display: none;"> <p id="reply_title">回复:</p> <div id="reply-content"> </div> </div> {% get_comment_form blog as comment_form %} {% for field in comment_form %} {{ field }} {% endfor %} <span id="comment-error" class="text-danger float-left"></span> <input type="submit" value="评论" class="btn btn-primary float-right"> </form> {% else %} 您尚未登录,登录之后方可评论 {# 提交登录的时候带上从哪里访问的路径 #} <a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a> <span> or </span> <a class="btn-danger btn" href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a> {% endif %} </div> <div class="-comment-area"> <h3 class="comment-area-title">评论列表</h3> <div id="comment-list"> {% get_comment_list blog as comments %} {% for comment in comments %} <div id="root-{{ comment.pk }}" class="comment"> <span>{{ comment.user.get_nickname_or_username }}</span> <span>{{ comment.comment_time|date:"Y-m-d H:i:s" }}</span> <div id="comment-{{ comment.pk }}">{{ comment.text|safe }}</div> {# 点赞 #} <div class="like" onclick="likeChange(this,'{% get_content_type comment %}',{{ comment.pk }})"> <i class="far fa-thumbs-up {% get_like_status comment %}"></i> <span class="liked-num">{% get_like_count comment %}</span> </div> <a href="javascript:reply({{ comment.pk }})">回复</a> {% for reply in comment.root_comment.all %} <div class="reply"> <span>{{ reply.user.get_nickname_or_username }}</span> <span>{{ reply.comment_time|date:"Y-m-d H:i:s" }}</span> <span>回复:</span><span>{{ reply.reply_to.get_nickname_or_username }}</span> <div id="comment-{{ reply.pk }}">{{ reply.text|safe }}</div> {# 点赞 #} <div class="like" onclick="likeChange(this,'{% get_content_type reply %}',{{ reply.pk }})"> <i class="far fa-thumbs-up {% get_like_status reply %}"></i> <span class="liked-num">{% get_like_count reply %}</span> </div> <a href="javascript:reply({{ reply.pk }})">回复</a> </div> {% endfor %} </div> {% empty %} <span id="no-comment">暂无评论</span> {% endfor %} </div> </div> </div> </div> </div> {% endblock %} {% block js %} <script> // 处理点赞 function likeChange(obj, content_type, object_id) { let is_like = obj.getElementsByClassName('active').length === 0; $.ajax({ url: "{% url 'like_change' %}", type: 'GET', data: { content_type: content_type, object_id: object_id, is_like: is_like, }, cache: false, success: function (data) { console.log(data); if (data['status'] === 'SUCCESS') { // 更新点赞状态 let element = $(obj.getElementsByClassName('fa-thumbs-up')); if (is_like) { element.addClass('active'); } else { element.removeClass('active'); } // 更新点赞数量 let like_num = $(obj.getElementsByClassName('liked-num')); like_num.text(data['liked_num']); } else { if (data['code'] === 400) { $('#login_model').modal('show'); } else { alert(data['msg']); } } }, error: function (xhr) { console.log(xhr); } }); } // 处理回复 function reply(reply_comment_id) { $('#reply_comment_id').val(reply_comment_id); let html = $('#comment-' + reply_comment_id).html(); $('#reply-content').html(html); $('#reply-content-container').show(); // 显示内容 // 滚动富文本编辑器 $('html').animate({scrollTop: $('#comment-form').offset().top - 60}, 300, function () { // 动画执行完毕后执行的方法 // 让富文本编辑器获得焦点 CKEDITOR.instances['id_text'].focus(); }); } function numFormat(num) { return ('00' + num).substr(-2); } function timeFormat(timestamp) { let datetime = new Date(timestamp * 1000); let year = datetime.getFullYear(); let month = numFormat(datetime.getMonth() + 1); let day = numFormat(datetime.getDate()); let hour = numFormat(datetime.getHours()); let minute = numFormat(datetime.getMinutes()); let second = numFormat(datetime.getSeconds()); return `${year}-${month}-${day} ${hour}:${minute}:${second}` } // 提交评论 $('#comment-form').submit(function () { // 获取错误框 let comment_error = $('#comment-error'); comment_error.text(''); // 更新数据到textarea CKEDITOR.instances['id_text'].updateElement(); let comment_text = CKEDITOR.instances['id_text'].document.getBody().getText().trim(); // 判断是否为空 if (!(CKEDITOR.instances['id_text'].document.getBody().find('img')['$'].length !== 0 || comment_text !== '')) { // 显示错误信息 comment_error.text('评论内容不能为空'); return false; } //异步提交 $.ajax({ url: "{% url 'update_comment' %}", type: 'POST', data: $(this).serialize(),// 序列化表单值 cache: false, // 关闭缓存 success: function (data) { let reply_comment = $('#reply_comment_id'); if (data['status'] === 'SUCCESS') { console.log(data); // 插入数据 // es6写法 let like_html = `<div class="like" onclick="likeChange(this,'${data['content_type']}',${data["pk"]})"> <i class="far fa-thumbs-up"></i> <span class="liked-num">0</span> </div>`; if (reply_comment.val() === '0') { // 插入评论 let comment_html = `<div id="root-${data["pk"]}" class="comment"> <span>${data["username"]}</span> <span>${timeFormat(data["comment_time"])}</span> <div id="comment-${data["pk"]}">${data["text"]}</div> ${like_html} <a href="javascript:reply(${data["pk"]})">回复</a> </div>`; $('#comment-list').prepend(comment_html); } else { // 插入回复 let reply_html = `<div class="reply"> <span>${data["username"]}</span> <span>${timeFormat(data["comment_time"])}</span> <span>回复:</span><span>${data["reply_to"]}</span> <div id="comment-${data["pk"]}">${data["text"]}</div> ${like_html} <a href="javascript:reply(${data["pk"]})">回复</a> </div>`; $('#root-' + data['root_pk']).append(reply_html); } // 清空编辑框的内容 CKEDITOR.instances['id_text'].setData(''); $('#reply-content-container').hide(); // 回复完隐藏掉要回复的内容 reply_comment.val('0'); // 将回复标志重置0 $('#no-comment').remove(); // 如果有没回复标志,清除掉5 comment_error.text('评论成功'); } else { // 显示错误信息 comment_error.text(data['message']) } }, error: function (xhr) { console.log(xhr); } }); return false; }); </script> <script> $(".nav-blog").addClass("active").siblings().removeClass("active"); </script> {% endblock %}
from django.http import JsonResponse from django.contrib.contenttypes.models import ContentType from .models import Comment from .forms import CommentForm def update_commit(requests): comment_form = CommentForm(requests.POST, user=requests.user) if comment_form.is_valid(): comment = Comment() comment.user = comment_form.cleaned_data['user'] comment.text = comment_form.cleaned_data['text'] comment.content_object = comment_form.cleaned_data['content_object'] parent = comment_form.cleaned_data['parent'] if parent is not None: comment.root = parent.root if parent.root is not None else parent comment.parent = parent comment.reply_to = parent.user comment.save() # 返回数据 data = { 'status': 'SUCCESS', 'username': comment.user.get_nickname_or_username(), 'comment_time': comment.comment_time.timestamp(), # 返回时间戳 'text': comment.text.strip(), 'reply_to': comment.reply_to.get_nickname_or_username() if parent is not None else '', 'pk': comment.pk, 'root_pk': comment.root.pk if comment.root is not None else '', 'content_type': ContentType.objects.get_for_model(comment).model, } else: data = { 'status': 'ERROR', 'message': list(comment_form.errors.values())[0][0], } return JsonResponse(data)
""" Django settings for myblog project. Generated by 'django-admin startproject' using Django 2.1.3. For more information on this file, see https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'ea+kzo_5k^6r7micfg@lar1(rfdc08@b4*+w5d11=0mp1p5ngr' # SECURITY WARNING: don't run with debug turned on in production!2. DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'ckeditor', 'ckeditor_uploader', 'blog.apps.BlogConfig', # 将自己创建的app添加到设置中 'read_statistics.apps.ReadStatisticsConfig', # 注册阅读统计app 'comment.apps.CommentConfig', # 注册评论 'likes.apps.LikesConfig', # 注册点赞 'user.apps.UserConfig', # 用户相关 ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'blog.middleware.mymiddleware.My404', # 添加自己的中间件 ] ROOT_URLCONF = 'myblog.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ os.path.join(BASE_DIR, 'templates'), ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'user.context_processors.login_model_form', # 自定义模板变量,直接用模板语言调用 ], }, }, ] WSGI_APPLICATION = 'myblog.wsgi.application' # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'myblogs', # 要连接的数据库,连接前需要创建好 'USER': 'root', # 连接数据库的用户名 'PASSWORD': 'felixwang', # 连接数据库的密码 'HOST': '127.0.0.1', # 连接主机,默认本级 'PORT': 3306 # 端口 默认3306 } } # Password validation # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/2.1/topics/i18n/ # LANGUAGE_CODE = 'en-us' # 语言 LANGUAGE_CODE = 'zh-hans' # TIME_ZONE = 'UTC' # 时区 TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True # 不考虑时区 USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] # media MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 配置ckeditor CKEDITOR_UPLOAD_PATH = 'upload/' # 自定义参数 EACH_PAGE_BLOGS_NUMBER = 7 # 设置数据库缓存 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'my_read_num_cache_table', } } # ckeditor 代码高亮,以及公式 CKEDITOR_CONFIGS = { 'default': { 'skin': 'moono', 'tabSpaces': 4, 'mathJaxLib': 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML', 'toolbar': ( ['div', 'Source', '-', 'Save', 'NewPage', 'Preview', '-', 'Templates'], ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Print', 'SpellChecker', 'Scayt'], ['Undo', 'Redo', '-', 'Find', 'Replace', '-', 'SelectAll', 'RemoveFormat', '-', 'Maximize', 'ShowBlocks', '-', "CodeSnippet", 'Mathjax', 'Subscript', 'Superscript'], ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'], ['Bold', 'Italic', 'Underline', 'Strike', '-'], ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', 'Blockquote'], ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], ['Link', 'Unlink', 'Anchor'], ['Image', 'Flash', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar', 'PageBreak'], ['Styles', 'Format', 'Font', 'FontSize'], ['TextColor', 'BGColor'],), 'extraPlugins': ','.join([ 'codesnippet', 'mathjax', 'dialog', 'dialogui', 'lineutils', ]), }, 'comment_ckeditor': { 'toolbar': 'custom', 'toolbar_custom': [ ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript'], ['TextColor', 'BGColor', 'RemoveFormat'], ['NumberedList', 'BulletedList'], ['Link', 'Unlink'], ['Smiley', 'SpecialChar', 'Blockquote'], ], 'width': 'auto', 'height': '180', 'tabSpace': 4, 'removePlugins': 'elementspath', 'resize_enabled': False, } } # 发送邮箱设置 MAIL_HOST = 'smtp.qq.com' # smtp服务地址 EMAIL_PORT = 465 # 端口号 EMAIL_HOST_USER = '1403179190@qq.com' # qq邮箱 EMAIL_HOST_PASSWORD = ''' # 如果是qq邮箱的话该密码是配置qq邮箱的SMTP功能的授权码 FROM_WHO = 'FCBlog' # 前缀
# -*- coding: utf-8 -*- # @Time : 18-11-27 上午11:07 # @Author : Felix Wang from PIL import Image, ImageDraw, ImageFont, ImageFilter from io import BytesIO import random from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage from email.utils import parseaddr, formataddr from email.header import Header import smtplib from hashlib import md5 class CheckCode: def __init__(self, font_file, font_size=36, width=240, height=60, char_length=4, start_color_num=0, end_color_num=255, is_simple=True, is_oncache=False): self.is_oncache = is_oncache self.is_simple = is_simple self.font_file = font_file self.font_size = font_size self.width = width self.height = height self.char_length = char_length self.start_num = start_color_num self.end_num = end_color_num # 定义使用Image类实例化一个长为120px,宽为30px,基于RGB的(255,255,255)颜色的图片 self.image = Image.new('RGB', (self.width, self.height), (255, 255, 255)) # 创建Font对象: self.font = ImageFont.truetype(self.font_file, self.font_size) # 创建Draw对象: self.draw = ImageDraw.Draw(self.image) # 双下划綫的变量在类中不能直接访问起到保护的作用 self.__code_text = [] def get_random_code(self, time=1): """ :param is_simple: 是否包含中文繁体字,默认不包含,True表示不包含 :param time: 返回多少个 :return: 返回一个随机字符列表 """ is_simple = self.is_simple codes = [] for i in range(time): num = chr(random.randint(0x30, 0x39)) # 随机生成数字 lowercase = chr(random.randint(0x61, 0x74)) # 随机生成小写字母 capcase = chr(random.randint(0x41, 0x5a)) # 随机生成大写字母 # Unicode编码的汉字,带繁体字 ,共2万多字 zh = chr(random.randint(0x4e00, 0x9fbf)) # gbk编码的简单汉字,无繁体字 head = random.randint(0xb0, 0xf7) body = random.randint(0xa1, 0xf9) # 在head区号为55的那一块最后5个汉字是乱码,为了方便缩减下范围 val = f'{head:x}{body:x}' ch = bytes.fromhex(val).decode('gb2312') if is_simple: # code = random.choice([ch, num, lowercase, capcase]) code = random.choice([ch, num, lowercase, capcase]) else: code = random.choice([zh, num, lowercase, capcase]) codes.append(code) return codes # 随机颜色: def rndColor(self, start, end, randomflag=True): """ :param start: :param end: :param randomflag: 是否返回随机参数, :return: """ return (random.randint(start, end), random.randint(start, end), random.randint(start, end)) def rotate(self): self.image.rotate(random.randint(0, 90), expand=0) # 随机点 def randPoint(self): return (random.randint(0, self.width), random.randint(0, self.height)) # 随机线 def randLine(self, num): draw = ImageDraw.Draw(self.image) for i in range(0, num): draw.line([self.randPoint(), self.randPoint()], self.rndColor(0, 255)) del draw # 获取验证码 def get_codes(self): return self.__code_text def draw_pic(self): # 填充每个像素: # 单一背景 color = self.rndColor(170, 255) # 可以把背景调浅色 for x in range(self.width): for y in range(self.height): self.draw.point((x, y), fill=color) # 输出文字: codes = self.get_random_code(time=self.char_length) self.__code_text = [] for ii in range(self.char_length): code = self.get_random_code()[0] self.__code_text.append(code) self.draw.text([random.randint(int((self.width / 2 - self.font_size / 2) / self.char_length * 2 * ii), int((self.width / 2 - self.font_size / 2) / self.char_length * 2 * ( ii + 1))), random.randint(0, self.height / 4)], code, font=self.font, fill=self.rndColor(0, 120)) # 把字体调深色 # self.rotate() # 画出随机线 self.randLine(10) # 模糊: # self.image = self.image.filter(ImageFilter.BLUR) if self.is_oncache: # 保存在缓存 f = BytesIO() self.image.save(f, 'jpeg') return f.getvalue() else: # 保存 self.image.save('{}.jpg'.format(''.join(self.get_codes())), 'jpeg') # 自动发邮件 class AutoSendEmail: def __init__(self, sender, password, title, from_who, recever, smtp_server="smtp.qq.com", port=465): """ :param sender: 邮件发送者 :param password: 密码 :param title: 邮件发送主题 :param from_who: 邮件来自谁 :param recever: 邮件接收者,可以是多个 :param smtp_server: 邮件服务器,默认qq邮箱服务器 :param port: 服务器端口qq邮箱默认端口为465 """ self.smtp_server = smtp_server # 使用qq转发需要用到,可以在QQ邮箱设置中查看并开通此转发功能 self.smtp_port = port # smtp默认的端口是465 # 接受者可以是多个,放在列表中 self.recever = recever self.sender = sender self.password = password # 该密码是配置qq邮箱的SMTP功能的授权码 self.msg = MIMEMultipart() self.msg['Subject'] = title # 邮件标题 self.msg['From'] = self._format_addr(u'{} <{}>'.format(from_who, self.sender)) # 添加文字信息 def addTextMsg(self, text): text_plain = MIMEText(text, 'plain', 'utf-8') self.msg.attach(text_plain) # 添加图片 def addImageMsg(self, imgPath): extend = imgPath.split('.')[-1] with open(imgPath, 'rb')as f: sendimagefile = f.read() filename = md5(sendimagefile).hexdigest() + '.' + extend image = MIMEImage(sendimagefile) image.add_header('Content-ID', '<image1>') image["Content-Disposition"] = u'attachment; filename={}'.format(filename) self.msg.attach(image) # 添加附件 def addFile(self, filePath): extend = filePath.split('.')[-1] with open(filePath, 'rb')as f: sendfile = f.read() filename = md5(sendfile).hexdigest() + '.' + extend # 构造附件 text_att = MIMEText(sendfile, 'base64', 'utf-8') text_att["Content-Type"] = 'application/octet-stream' text_att["Content-Disposition"] = u'attachment; filename="{}"'.format(filename) self.msg.attach(text_att) # 添加html格式 def addHtml(self, html): # 构造html # 发送正文中的图片:由于包含未被许可的信息,网易邮箱定义为垃圾邮件,报554 DT:SPM :<p><img src="cid:image1"></p> text_html = MIMEText(html, 'html', 'utf-8') self.msg.attach(text_html) # 格式化邮件地址 def _format_addr(self, s): name, address = parseaddr(s) return formataddr((Header(name, 'utf-8').encode(), address)) # 发送邮件 def sendEmail(self): server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) # 链接服务器 server.set_debuglevel(1) # 打印出和SMTP服务器交互的信息 server.login(self.sender, self.password) # 登录 server.sendmail(self.sender, self.recever, self.msg.as_string()) # 发送邮件 server.quit() # 退出 print('邮件发送成功') if __name__ == '__main__': smtp_server = "smtp.qq.com" # smtp服务地址 port = 465 # 端口号 recever = ['1403179190@qq.com'] # 接收人列表可以是多个 sender = "1403179190@qq.com" # 发送人邮箱 password = "" # 如果是qq邮箱的话该密码是配置qq邮箱的SMTP功能的授权码 title = '验证码' from_who = 'felixCRM' # 发送人姓名 # 实例化对象 autoEmail = AutoSendEmail(sender=sender, recever=recever, password=password, title=title, from_who=from_who, smtp_server=smtp_server, port=port) html = """ <html> <head></head> <body> <p>Hi!<br> 欢迎注册felixCRM系统! <br> Here is the <a href="http://www.baidu.com">link</a> you wanted.<br> </p> <img src="http://img.zcool.cn/community/01f09e577b85450000012e7e182cf0.jpg@1280w_1l_2o_100sh.jpg"></img> </body> </html> """ # 以html的形式发送文字,推荐这个,因为可以添加图片等 autoEmail.addHtml(html) # 发送邮件 try: autoEmail.sendEmail() except Exception as e: print(e) print('邮件发送失败') if __name__ == '__main__': # 这里的字体采用能识别中文的字体 # font_file = 'C:WindowsFontssimhei.ttf' # windows使用这个 font_file = '/home/felix/.local/share/fonts/SIMHEI.TTF' checkcode = CheckCode(font_file=font_file, is_simple=True, char_length=random.randint(3, 5)) # 生成多张验证码 for i in range(1): checkcode.draw_pic() # 生成的验证码的内容 codes = checkcode.get_codes() print(i, codes)
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <!-- 根据屏幕自动响应布局 --> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title> {# 用来放标题 #} {% block title %} {% endblock %} </title> {# 加载css代码 #} <link rel="stylesheet" href="{% static 'bootstrap4.1/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/base.css' %}"> {# js代码放在后面可以增加性能 #} <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="{% static 'js/jquery-3.3.1.min.js' %}"></script> <script src="{% static 'bootstrap4.1/popper.min.js' %}"></script> <script src="{% static 'bootstrap4.1/bootstrap.min.js' %}"></script> {% block header_extends %} {# 用来做头部扩展,如加载某些静态文件 #} {% endblock %} </head> <body> {# 导航栏 #} <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top"> <a class="navbar-brand" href="{% url 'home' %}">Felix Blog</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item nav-home"><a href="{% url 'home' %}" class="nav-link">首页</a></li> <li class="nav-item nav-blog"><a href="{% url 'blog_list' %}" class="nav-link">博客</a></li> </ul> <ul class="navbar-nav"> {% if not user.is_authenticated %} <li class="nav-item"> <a class="nav-link" href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a> </li> {% else %} <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {{ user.username }} {% if user.has_nickname %} ({{ user.get_nickname }}) {% endif %} </a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{% url 'user_info' %}">个人资料</a> {% if user.is_staff or user.is_superuser %} <a class="dropdown-item" href="{% url 'admin:index' %}">后台管理</a> {% endif %} <div class="dropdown-divider"></div> <a class="dropdown-item" href="{% url 'logout' %}?from={{ request.get_full_path }}">登出</a> </div> </li> {% endif %} </ul> </div> </nav> {# 用来放内容 #} {% block content %} {% endblock %} <!-- Modal 登录模态框 --> <div class="modal fade" id="login_model" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <form id="login_model_form" action="" method="post"> <div class="modal-header"> <h5 class="modal-title">登录</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> {% csrf_token %} {% for field in login_model_form %} <label for="{{ field.id_for_label }}">{{ field.label }}</label> {{ field }} {% endfor %} <span id="login_model_tip" class="text-danger"></span> </div> <div class="modal-footer"> <button type="submit" class="btn btn-primary">登录</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button> </div> </form> </div> </div> </div> <script> $('#login_model_form').submit(function (event) { $('#login_model_tip').text(''); event.preventDefault(); // 阻止原事件提交 $.ajax({ url: '{% url 'login_for_model' %}', type: 'POST', data: $(this).serialize(), cache: false, success: function (data) { if (data['status'] === 'SUCCESS') { window.location.reload(); } else { $('#login_model_tip').text('用户名或密码不正确') } } }); }) </script> {# 导入资源建议放在js代码前 #} {# 用来放js代码 #} {% block js %} {% endblock %} </body> </html>
{% extends 'base.html' %} {% block title %} {{ page_title }} {% endblock %} {% block content %} <div class="container"> <div class="col-xl-6 offset-xl-3"> <div class="card"> <h5 class="card-header">{{ form_title }}</h5> <div class="card-body"> <form action="" method="post"> {% csrf_token %} {% for field in form %} {% if not field.is_hidden %} <label for="{{ field.id_for_label }}">{{ field.label }}</label> {% endif %} {{ field }} <p class="text-danger">{{ field.errors.as_text }}</p> {% endfor %} <span id="error-tip" class="text-danger">{{ form.non_field_errors }}</span> <div class="clearfix"></div> <div class="float-left"> {% block other-buttons %}{% endblock %} </div> <div class="float-right"> <input type="submit" value="{{ submit_text }}" class="btn btn-primary"> <button class="btn" onclick="window.location.href='{{ return_back_url }}'">返回</button> </div> </form> </div> </div> </div> </div> {% endblock %} {% block js %} {# 将首页这个按钮设置激活状态 #} <script> $(".nav-home").addClass("active").siblings().removeClass("active"); </script> {% endblock %}
{% extends 'form.html' %} {% block other-buttons %} <div id="send_code" class="btn btn-primary">发送验证码</div> {% endblock %} {% block js %} <script> $("#send_code").click(function () { if ($(this).hasClass('disabled')) { return false; } let email = $('#id_email').val(); if (email === '') { $('#error-tip').text('邮箱不能为空'); return false } let re_email = /^([a-zA-Z0-9]+[_|\_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,3}$/; if (!re_email.test(email)) { alert('邮箱格式不正确'); return false } // 发送验证码 $.ajax({ url: "{% url 'send_verification_code' %}", type: 'GET', data: { 'email': email, }, cache: false, success: function (data) { if (data['status'] === 'ERRORS') { alert(data['msg']); } else { alert(data['msg']); } } }); // # 把按钮变灰 $(this).addClass('disabled'); $(this).attr("disabled", true); let time = 60; let interval = setInterval(() => { time -= 1; $(this).text(`再次发送(${time}s)`); if (time <= 0) { // 时间等于0,进行复原 clearInterval(interval); $(this).removeClass('disabled'); $(this).attr('disabled', false); $(this).text('再次发送'); return false; } }, 1000); }); </script> {% endblock %}
{% extends 'base.html' %} {% block title %} 个人资料 {% endblock %} {% block content %} <div class="container"> <div class="col-xl-8 offset-xl-2"> <h2>{{ user.username }}</h2> {% if user.is_authenticated %} <ul> <li>昵称:{{ user.get_nickname }} <a href="{% url 'change_nickname' %}?from={{ request.get_full_path }}">修改昵称</a></li> <li> 邮箱: {% if user.email %} {{ user.email }} {% else %} 未绑定 <a href="{% url 'bind_email' %}?from={{ request.get_full_path }}">绑定邮箱</a> {% endif %} </li> <li>上一次登录时间:{{ user.last_login|date:"Y-m-d H:i:s" }}</li> <li><a href="">修改密码</a></li> </ul> {% else %} {# 未登录跳转到首页 #} <script> window.location.href = '{% url 'home' %}' </script> {% endif %} </div> </div> </div> {% endblock %} {% block js %} {# 将首页这个按钮设置激活状态 #} <script> $(".nav-home").addClass("active").siblings().removeClass("active"); </script> {% endblock %}
from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User from .models import Profile # Define an inline admin descriptor for Employee model # which acts a bit like a singleton class ProfileInline(admin.StackedInline): model = Profile can_delete = False # Define a new User admin class UserAdmin(BaseUserAdmin): inlines = (ProfileInline,) list_display = ('username', 'nickname', 'email', 'is_staff', 'is_active', 'is_superuser') def nickname(self, obj): return obj.profile.nickname nickname.short_description = '昵称' # 后台字段 # Re-register UserAdmin admin.site.unregister(User) admin.site.register(User, UserAdmin) @admin.register(Profile) class ProfileAdmin(admin.ModelAdmin): list_display = ('user', 'nickname')
# -*- coding: utf-8 -*- # @Time : 18-11-20 下午8:10 # @Author : Felix Wang import re from django import forms from django.contrib import auth from django.contrib.auth.models import User class LoginForm(forms.Form): username = forms.CharField(label='用户名', required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入用户名'})) # widget指定input标签类型 password = forms.CharField(label='密码', widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '请输入密码'})) def clean(self): # 验证数据 username = self.cleaned_data['username'] password = self.cleaned_data['password'] user = auth.authenticate(username=username, password=password) if user is None: raise forms.ValidationError('用户名或密码错误') self.cleaned_data['user'] = user # 将验证过的user放入clean_data return self.cleaned_data class RegisterForm(forms.Form): # 用户名字段 username = forms.CharField(label='用户名', max_length=30, min_length=3, required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入用户名'})) # 邮箱字段 email = forms.EmailField(label='邮箱', min_length=3, required=True, widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': '请输入邮箱'})) # 密码字段 password = forms.CharField(label='密码', min_length=6, required=True, widget=forms.PasswordInput( attrs={'class': 'form-control', 'placeholder': '请输入密码'})) # 再次输入密码 password_again = forms.CharField(label='确认密码', min_length=6, required=True, widget=forms.PasswordInput( attrs={'class': 'form-control', 'placeholder': '请再输入一次密码'})) def clean_username(self): username = self.cleaned_data['username'] if User.objects.filter(username=username).exists(): raise forms.ValidationError('用户名已存在') return username def clean_email(self): email = self.cleaned_data['email'] if User.objects.filter(email=email).exists(): raise forms.ValidationError('邮箱已存在') if not re.match(r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}.[com,cn,net]{1,3}$', email): raise forms.ValidationError('邮箱格式错误') return email def clean_password_again(self): password = self.cleaned_data['password'] password_again = self.cleaned_data['password_again'] if password != password_again: raise forms.ValidationError('两次输入的密码不一致') return password_again class ChangeNicknameForm(forms.Form): nickname_new = forms.CharField( label='新的昵称', max_length=20, widget=forms.TextInput( attrs={ 'class': 'form-control', 'placeholder': '请输入新的昵称', } ) ) def __init__(self, *args, **kwargs): if 'user' in kwargs: self.user = kwargs.pop('user') super().__init__(*args, **kwargs) # 表单验证 def clean(self): # 判断用户是否登录 if self.user.is_authenticated: self.cleaned_data['user'] = self.user else: raise forms.ValidationError('用户尚未登录') return self.cleaned_data def clean_nickname_new(self): nickname_new = self.cleaned_data.get('nickname_new', '').strip() if nickname_new == '': raise forms.ValidationError('新的昵称不能为空') return nickname_new class BindEmailForm(forms.Form): email = forms.EmailField( label='邮箱', widget=forms.EmailInput( attrs={ 'class': 'form-control', 'placeholder': '请输入邮箱', } ) ) verification_code = forms.CharField( label='验证码', max_length=20, required=False, widget=forms.TextInput( attrs={ 'class': 'form-control', 'placeholder': '请输入验证码', } ) ) def __init__(self, *args, **kwargs): if 'requests' in kwargs: self.requests = kwargs.pop('requests') super().__init__(*args, **kwargs) # 表单验证 def clean(self): # 判断用户是否登录 if self.requests.user.is_authenticated: self.cleaned_data['user'] = self.requests.user else: raise forms.ValidationError('用户尚未登录') # 判断用户是否已经绑定邮箱 if self.requests.user.email != '': raise forms.ValidationError('你已经绑定邮箱') # 判断验证码 code = self.requests.session.get('bind_email_code', '').upper() print('code', code) verification_code = self.cleaned_data.get('verification_code', '').upper() print('verification_code', verification_code) if code != verification_code or code == '': raise forms.ValidationError('验证码不正确') return self.cleaned_data def clean_email(self): email = self.cleaned_data['email'] if User.objects.filter(email=email).exists(): raise forms.ValidationError('该邮箱已经被绑定') if not re.match(r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}.[com,cn,net]{1,3}$', email): raise forms.ValidationError('邮箱格式错误') return email def clean_verification_code(self): verification_code = self.cleaned_data.get('verification_code', '').strip().upper() if verification_code == '': raise forms.ValidationError('验证码不能为空') return verification_code
from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) nickname = models.CharField(max_length=20, verbose_name='昵称', default='') def __str__(self): return '<Profile: {} for {}>'.format(self.nickname, self.user.username) def get_nickname(self): if Profile.objects.filter(user=self).exists(): profile = Profile.objects.get(user=self) return profile.nickname else: return '' def get_nickname_or_username(self): if Profile.objects.filter(user=self).exists(): profile = Profile.objects.get(user=self) return profile.nickname else: return self.username def has_nickname(self): return Profile.objects.filter(user=self).exists() User.get_nickname = get_nickname # 动态绑定方法 User.has_nickname = has_nickname # 动态绑定方法 User.get_nickname_or_username = get_nickname_or_username # 动态绑定方法
# -*- coding: utf-8 -*- # @Time : 18-11-4 下午5:22 # @Author : Felix Wang from django.urls import path from . import views urlpatterns = [ path('login/', views.login, name='login'), # 登录 path('logout/', views.logout, name='logout'), # 登录 path('login_for_model/', views.login_for_model, name='login_for_model'), # 登录 path('register/', views.register, name='register'), # 注册 path('user_info/', views.user_info, name='user_info'), # 用户信息 path('change_nickname/', views.change_nickname, name='change_nickname'), # 更改昵称 path('bind_email/', views.bind_email, name='bind_email'), # 更改昵称 path('send_verification_code/', views.send_verification_code, name='send_verification_code'), # 更改昵称 ]
# -*- coding: utf-8 -*- # @Time : 18-11-7 下午4:12 # @Author : Felix Wang import random import re import time from django.shortcuts import render, redirect from django.http import JsonResponse from django.contrib import auth from django.contrib.auth.models import User from django.urls import reverse from django.conf import settings from .forms import LoginForm, RegisterForm, ChangeNicknameForm, BindEmailForm from .models import Profile from myblog.utils import AutoSendEmail def login(requests): # 如果是form表单提交验证登录 if requests.method == 'POST': login_form = LoginForm(requests.POST) if login_form.is_valid(): # 验证是否通过 # 因为在form表单验证过了,所以不用自己再验证 user = login_form.cleaned_data.get('user') auth.login(requests, user) return redirect(requests.GET.get('from', reverse('home'))) else: login_form.add_error(None, '用户名或密码不正确') else: login_form = LoginForm() context = { 'login_form': login_form, } return render(requests, 'user/login.html', context) def login_for_model(requests): login_form = LoginForm(requests.POST) # 如果是form表单提交验证登录 if login_form.is_valid(): # 验证是否通过 # 因为在form表单验证过了,所以不用自己再验证 user = login_form.cleaned_data.get('user') auth.login(requests, user) data = { 'status': 'SUCCESS', } else: data = { 'status': 'ERROR', } return JsonResponse(data) def register(requests): if requests.method == 'POST': reg_form = RegisterForm(requests.POST) if reg_form.is_valid(): username = reg_form.cleaned_data['username'] email = reg_form.cleaned_data['email'] password = reg_form.cleaned_data['password'] # 创建用户 user = User.objects.create_user(username=username, email=email, password=password) user.save() # 登录用户 user = auth.authenticate(username=username, password=password) auth.login(requests, user) # 登录之后跳转 return redirect(requests.GET.get('from', reverse('home'))) else: reg_form = RegisterForm() context = { 'reg_form': reg_form, } return render(requests, 'user/register.html', context) def logout(requests): auth.logout(requests) return redirect(requests.GET.get('from', reverse('home'))) def user_info(requests): context = {} return render(requests, 'user/user_info.html', context) def change_nickname(requests): redirect_to = requests.GET.get('from', reverse('home')) if requests.method == 'POST': form = ChangeNicknameForm(requests.POST, user=requests.user) if form.is_valid(): nickname_new = form.cleaned_data['nickname_new'] profile, created = Profile.objects.get_or_create(user=requests.user) profile.nickname = nickname_new profile.save() return redirect(redirect_to) else: form = ChangeNicknameForm() context = { 'submit_text': '修改', 'page_title': '修改昵称', 'form_title': '修改昵称', 'form': form, 'return_back_url': redirect_to, } return render(requests, 'form.html', context) def bind_email(requests): redirect_to = requests.GET.get('from', reverse('home')) if requests.method == 'POST': form = BindEmailForm(requests.POST, requests=requests) if form.is_valid(): email = form.cleaned_data['email'] requests.user.email = email requests.user.save() return redirect(redirect_to) else: form = BindEmailForm() context = { 'submit_text': '绑定邮箱', 'page_title': '绑定邮箱', 'form_title': '绑定', 'form': form, 'return_back_url': redirect_to, } return render(requests, 'user/bind_email.html', context) def send_verification_code(requests): email = requests.GET.get('email', '') if re.match(r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}.[com,cn,net]{1,3}$', email): # 生成验证码 all_codes = list(range(0x30, 0x39)) + list(range(0x61, 0x74)) + list(range(0x41, 0x5a)) # 大写,小写和数字 code = ''.join([chr(random.choice(all_codes)) for i in range(6)]) now = int(time.time()) send_code_time = requests.session.get('send_code_time', 0) if now - send_code_time < 60: data = { 'status': 'ERROR', } else: requests.session['bind_email_code'] = code requests.session['send_code_time'] = send_code_time title = '验证码' auto_email = AutoSendEmail(sender=settings.EMAIL_HOST_USER, recever=[email], password=settings.EMAIL_HOST_PASSWORD, title=title, from_who=settings.FROM_WHO, smtp_server=settings.MAIL_HOST, port=settings.EMAIL_PORT) html = """ <html> <head></head> <body> <p>Hi!<br> 非常感谢您绑定邮箱! <br> 本次的验证码是:{},请不要透露给其他人! <br> </p> <img style="180px;height:240px" src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1541440161574&di=fd6156e441788866ffbd6c654d75fa23&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201507%2F22%2F20150722222322_Ky8Nj.jpeg" /> </body> </html> """.format(code) # 以html的形式发送文字,推荐这个,因为可以添加图片等 auto_email.addHtml(html) # 发送邮件 try: auto_email.sendEmail() data = { 'status': 'SUCCESS', 'msg': '邮件发送成功', } except Exception as e: data = { 'status': 'ERRORS', 'msg': '邮件发送失败', } else: data = { 'status': 'ERRORS', 'msg': '邮箱格式不正确', } return JsonResponse(data)