新增注册邀请码功能,并优化部分样式

This commit is contained in:
yangjian 2020-02-22 14:22:44 +08:00
parent 3e8678e363
commit f2f4a97c37
17 changed files with 416 additions and 44 deletions

1
.gitignore vendored
View File

@ -57,6 +57,7 @@ coverage.xml
*.log
local_settings.py
db.sqlite3
media
# Flask stuff:
instance/

View File

@ -1,5 +1,10 @@
## 版本更新记录
### v0.2.10 2020-02-22
- 优化修改文档页面的文档状态提示;
- 新增注册邀请码功能,可在后台配置管理;
### v0.2.9 2020-02-17
- 优化文本编辑器排版

View File

@ -25,7 +25,7 @@ SECRET_KEY = '5&71mt9@^58zdg*_!t(x6g14q*@84d%ptr%%s6e0l50zs0we3d'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
VERSIONS = '0.2.9'
VERSIONS = '0.2.10'
ALLOWED_HOSTS = ['*']

View File

@ -4,44 +4,46 @@
![Mrdoc首页](./docs/mrdoc_2019080101.gif)
**手机端文档阅读界面:**
![移动端](./docs/mrdoc-2019-12-15_204807.jpg)
## 介绍
基于Python的一个简单文档写作系统。
当前版本为:**v0.2.9**,版本发布时间为**2020-02-17**,更新记录详见:[CHANGES.md](./CHANGES.md)
州的先生zmister.com自用并完全开源、基于Python编写的文档写作系统。
当前版本为:**v0.2.10**,版本发布时间为**2020-02-22**,更新记录详见:[CHANGES.md](./CHANGES.md)
MrDoc拥有以下特点
- 基于Django自带的用户模型实现简单高效的用户管理支持用户注册、用户登录、管理员等控制等功能
- 基于Editormd的Markdown编辑器支持Markdown语法的文档写作支持图片粘贴上传
- 提供文档模板功能,支持文档模板的创建、修改;
- 仿GitBook文档阅读页面支持文档阅读页面的字体缩放字体类型修改
- 支持三级目录层级显示;
- 支持文集导出为markdown文本格式.md文件
- 支持基于文集的权限控制提供公开、私密、指定用户可见、访问码可见4种权限模式
- 使用方便、二次开发修改也方便;
- 站点与用户系统简洁
- 基于Django自带的用户模型实现简单高效的用户管理支持用户注册、用户登录、管理员等控制等功能
- 支持全站关闭注册;
- 支持注册邀请码配置;
- 支持广告位自定义配置;
- 支持统计代码自定义配置;
- 文档系统清晰
- 基于文集的文档撰写和阅读;
- 基于Editormd的Markdown编辑器支持Markdown语法的文档写作支持图片粘贴上传支持从本地MD文件中插入内容
- 提供文档模板功能,支持文档模板的创建、修改;
- 仿GitBook文档阅读页面支持文档阅读页面的字体缩放字体类型修改页面社交分享良好的移动端阅读体验
- 支持三级目录层级显示;
- 支持文集导出为markdown文本格式.md文件
- 基于文集进行权限控制提供公开、私密、指定用户可见、访问码可见4种权限模式
- 二次开发方便
- 使用Django传统的MTV开发模式路由、视图函数、模型易于理解
- 使用非前端工程化构建前端页面主要使用Layui进行页面布局和展示方便改动
在开发过程中参考和借鉴了GitBook、ShowDoc、Wordbook等应用的功能和样式。
## 软件架构
在开发过程中参考和借鉴了GitBook、ShowDoc、Wordbook等应用和网站的功能与样式。
后端基于Python Web框架Django
## 网站架构
编程语言Python 3
后端框架Django 2.1
前端UI库LayUI 2.5.4
MarkDown编辑器Editormd
页面社交分享Share.js
Markdown科学公式Katex.js
- 编程语言Python 3
- 后端Web框架Django 2.1
- 前端UI库Layui 2.5.4
- JS库Jquery
- MarkDown编辑器Editormd
- 页面社交分享Share.js
- Markdown科学公式Katex.js
## 安装教程
@ -51,7 +53,7 @@ pip install -r requirements.txt
```
### 2、配置数据库信息
默认情况下MrDoc使用Django的SQLite数据库如果你使用的是MrDoc源码附带的Sqlite数据库则无需另外配置数据库。
默认情况下MrDoc使用Django的SQLite数据库在旧版本MrDoc附带了一个Sqlit数据库如果你使用的是MrDoc源码附带的Sqlite数据库或使用Sqlite数据库则无需另外配置数据库。
如果有配置其他数据库的需求,请在/MrDoc/MrDoc目录下打开settings.py文件在约80行的位置将如下代码
```
DATABASES = {

View File

@ -0,0 +1,33 @@
# Generated by Django 2.1 on 2020-02-21 20:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('app_admin', '0005_auto_20191125_2155'),
]
operations = [
migrations.CreateModel(
name='RegisterCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=10, unique=True, verbose_name='注册邀请码')),
('all_cnt', models.IntegerField(default=1, verbose_name='有效注册数量')),
('used_cnt', models.IntegerField(default=0, verbose_name='已使用数量')),
('status', models.IntegerField(default=1, verbose_name='注册码状态')),
('user_list', models.CharField(blank=True, max_length=500, null=True, verbose_name='使用此注册码的用户')),
('create_time', models.DateTimeField(auto_now=True, verbose_name='创建时间')),
('create_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': '注册邀请码',
'verbose_name_plural': '注册邀请码',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1 on 2020-02-22 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app_admin', '0006_registercode'),
]
operations = [
migrations.AlterField(
model_name='registercode',
name='user_list',
field=models.CharField(blank=True, default='', max_length=500, null=True, verbose_name='使用此注册码的用户'),
),
]

View File

@ -1,4 +1,6 @@
from django.db import models
from django.contrib.auth.models import User
# 系统设置项模型
class SysSetting(models.Model):
@ -27,4 +29,24 @@ class EmaiVerificationCode(models.Model):
class Meta:
verbose_name = '电子邮件验证码'
verbose_name_plural = verbose_name
# 用户注册邀请码模型
class RegisterCode(models.Model):
code = models.CharField(verbose_name="注册邀请码",max_length=10,unique=True)
# 注册码的有效注册数量表示注册码最多能够被使用多少次默认为1
all_cnt = models.IntegerField(verbose_name="有效注册数量",default=1)
# 注册码的已使用数量其值小于等于有效注册数量默认为0
used_cnt = models.IntegerField(verbose_name='已使用数量',default=0)
# 注册码状态0表示数据已满1表示有效默认为1
status = models.IntegerField(verbose_name="注册码状态",default=1)
user_list = models.CharField(verbose_name="使用此注册码的用户",default='',max_length=500,blank=True,null=True)
create_user = models.ForeignKey(User,on_delete=models.CASCADE)
create_time = models.DateTimeField(auto_now=True,verbose_name='创建时间')
def __str__(self):
return self.code
class Meta:
verbose_name = '注册邀请码'
verbose_name_plural = verbose_name

View File

@ -18,5 +18,6 @@ urlpatterns = [
path('check_code/',views.check_code,name='check_code'), # 注册验证码
path('forget_pwd/',views.forget_pwd,name='forget_pwd'), # 忘记密码
path('send_email_vcode/',views.send_email_vcode,name='send_email_vcode'), # 忘记密码发送邮件验证码
path('admin_register_code/',views.admin_register_code,name='register_code_manage'), # 注册邀请码管理
]

View File

@ -6,7 +6,8 @@ from django.contrib.auth.models import User # Django默认用户模型
from django.contrib.auth.decorators import login_required # 登录需求装饰器
from django.core.paginator import Paginator,PageNotAnInteger,EmptyPage,InvalidPage # 后端分页
from app_admin.decorators import superuser_only,open_register
import json,datetime,hashlib
from django.core.exceptions import ObjectDoesNotExist
import json,datetime,hashlib,random
from app_doc.models import *
from app_admin.models import *
from app_admin.utils import *
@ -68,20 +69,29 @@ def register(request):
email = request.POST.get('email',None)
password = request.POST.get('password',None)
checkcode = request.POST.get("check_code",None)
register_code = request.POST.get("register_code",None)
is_register_code = SysSetting.objects.filter(types='basic', name='enable_register_code', value='on')
if is_register_code.count() > 0: # 开启了注册码设置
try:
register_code_value = RegisterCode.objects.get(code=register_code,status=1)
except ObjectDoesNotExist:
errormsg = '注册码无效!'
return render(request, 'register.html', locals())
# 判断是否输入了用户名、邮箱和密码
if username and email and password:
if '@'in email:
email_exit = User.objects.filter(email=email)
username_exit = User.objects.filter(username=username)
if email_exit.count() > 0:
if email_exit.count() > 0: # 验证电子邮箱
errormsg = '此电子邮箱已被注册!'
return render(request, 'register.html', locals())
elif username_exit.count() > 0:
elif username_exit.count() > 0: # 验证用户名
errormsg = '用户名已被使用!'
return render(request, 'register.html', locals())
elif len(password) < 6:
elif len(password) < 6: # 验证密码长度
errormsg = '密码必须大于等于6位'
return render(request, 'register.html', locals())
elif checkcode != request.session['CheckCode'].lower():
elif checkcode != request.session['CheckCode'].lower(): # 验证验证码
errormsg = "验证码错误"
return render(request, 'register.html', locals())
else:
@ -90,11 +100,27 @@ def register(request):
user.save()
# 登录用户
user = authenticate(username=username, password=password)
# 注册码数据更新
if is_register_code.count() > 0:
r_all_cnt = register_code_value.all_cnt # 注册码的最大使用次数
r_used_cnt = register_code_value.used_cnt + 1 # 更新注册码的已使用次数
r_use_user = register_code_value.user_list # 注册码的使用用户
if r_used_cnt >= r_all_cnt: # 如果注册码已使用次数大于等于注册码的最大使用次数,则注册码失效
RegisterCode.objects.filter(code=register_code).update(
status=0,# 注册码状态设为失效
used_cnt = r_used_cnt, # 更新注册码的已使用次数
user_list = r_use_user + email + ',',
)
else:
RegisterCode.objects.filter(code=register_code).update(
used_cnt=r_used_cnt, # 更新注册码的已使用次数
user_list = r_use_user + email + ',',
)
if user.is_active:
login(request, user)
return redirect('/')
else:
errormsg = '用户被禁用!'
errormsg = '用户被禁用,请联系管理员'
return render(request, 'register.html', locals())
else:
errormsg = '请输入正确的电子邮箱格式!'
@ -388,6 +414,62 @@ def admin_doctemp(request):
return render(request,'app_admin/admin_doctemp.html',locals())
# 管理员后台 - 注册邀请码管理
@superuser_only
def admin_register_code(request):
# 返回注册邀请码管理页面
if request.method == 'GET':
register_codes = RegisterCode.objects.all()
paginator = Paginator(register_codes, 10)
page = request.GET.get('page', 1)
try:
codes = paginator.page(page)
except PageNotAnInteger:
codes = paginator.page(1)
except EmptyPage:
codes = paginator.page(paginator.num_pages)
return render(request,'app_admin/admin_register_code.html',locals())
elif request.method == 'POST':
types = request.POST.get('types',None)
if types is None:
return JsonResponse({'status':False,'data':'参数错误'})
# types表示注册码操作的类型1表示新增、2表示删除
if int(types) == 1:
try:
all_cnt = int(request.POST.get('all_cnt',1)) # 注册码的最大使用次数
is_code = False
while is_code is False:
code_str = '0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
random_code = ''.join(random.choices(code_str, k=10))
random_code_used = RegisterCode.objects.filter(code=random_code).count()
if random_code_used > 0: # 已存在此注册码,继续生成一个注册码
is_code = False
else:# 数据库中不存在此注册码,跳出循环
is_code = True
# 创建一个注册码
RegisterCode.objects.create(
code = random_code,
all_cnt = all_cnt,
create_user = request.user
)
return JsonResponse({'status':True,'data':'新增成功'})
except Exception as e:
return JsonResponse({'status': False,'data':'系统异常'})
elif int(types) == 2:
code_id = request.POST.get('code_id',None)
try:
register_code = RegisterCode.objects.get(id=int(code_id))
register_code.delete()
return JsonResponse({'status':True,'data':'删除成功'})
except ObjectDoesNotExist:
return JsonResponse({'status':False,'data':'注册码不存在'})
except:
return JsonResponse({'status':False,'data':'系统异常'})
else:
return JsonResponse({'status':False,'data':'类型错误'})
else:
return JsonResponse({'status': False,'data':'方法错误'})
# 普通用户修改密码
@login_required()
def change_pwd(request):
@ -429,11 +511,12 @@ def admin_setting(request):
types = request.POST.get('type',None)
# 基础设置
if types == 'basic':
close_register = request.POST.get('close_register',None)
static_code = request.POST.get('static_code',None)
ad_code = request.POST.get('ad_code',None)
beian_code = request.POST.get('beian_code',None)
enbale_email = request.POST.get("enable_email",None)
close_register = request.POST.get('close_register',None) # 禁止注册
static_code = request.POST.get('static_code',None) # 统计代码
ad_code = request.POST.get('ad_code',None) # 广告代码
beian_code = request.POST.get('beian_code',None) # 备案号
enbale_email = request.POST.get("enable_email",None) # 启用邮箱
enable_register_code = request.POST.get('enable_register_code',None) # 注册邀请码
# 更新开放注册状态
SysSetting.objects.update_or_create(
name='close_register',
@ -459,6 +542,11 @@ def admin_setting(request):
name='enable_email',
defaults={'value': enbale_email, 'types': 'basic'}
)
# 更新注册码启停状态
SysSetting.objects.update_or_create(
name = 'enable_register_code',
defaults= {'value': enable_register_code, 'types':'basic'}
)
return render(request,'app_admin/admin_setting.html',locals())
elif types == 'email':

View File

@ -398,6 +398,11 @@ def manage_doc(request):
docs.kw = search_kw
else:
doc_list = Doc.objects.filter(create_user=request.user).order_by('-modify_time')
all_cnt = doc_list.count() # 所有文档数量
published_doc_cnt = Doc.objects.filter(create_user=request.user,status=1).count() # 已发布文档数量
draft_doc_cnt = Doc.objects.filter(create_user=request.user,status=0).count() # 草稿文档数据
pro_list = Project.objects.filter(create_user=request.user)
paginator = Paginator(doc_list, 10)
page = request.GET.get('page', 1)
try:

View File

@ -55,7 +55,10 @@
<a href="{% url 'user_manage' %}"><i class="layui-icon layui-icon-user"></i> 用户管理</a>
</li>
<li class="layui-nav-item layui-nav-itemed">
<a href="{% url 'sys_setting' %}"><i class="layui-icon layui-icon-set-sm"></i> 应用设置</a>
<a href="{% url 'sys_setting' %}"><i class="layui-icon layui-icon-set-sm"></i> 站点设置</a>
</li>
<li class="layui-nav-item layui-nav-itemed">
<a href="{% url 'register_code_manage' %}"><i class="layui-icon layui-icon-engine"></i> 注册码管理</a>
</li>
{% endif %}
</ul>

View File

@ -0,0 +1,157 @@
{% extends 'app_admin/admin_base.html' %}
{% load staticfiles %}
{% block title %}注册码管理{% endblock %}
{% block content %}
<div class="layui-card-header" style="margin-bottom: 10px;">
<div class="layui-row">
<span style="font-size:18px;">注册邀请码管理
</span>
</div>
</div>
<div class="layui-row">
<form action="{% url 'project_manage' %}" method="get">
<div class="layui-form-item">
<!--<div class="layui-input-inline">-->
<!--<input type="text" name="kw" id="kw" placeholder="输入文集内容" autocomplete="off" class="layui-input">-->
<!--</div>-->
<!--<button class="layui-btn layui-btn-normal" type="submit">搜索</button>-->
<button class="layui-btn layui-btn-normal" onclick="createRegisterCode()" type="button"><i class="layui-icon layui-icon-addition"></i>新增注册码</button>
</div>
</form>
</div>
<div class="layui-row" lay-skin="line">
<table class="layui-table" id="register-code-list" lay-skin="">
<colgroup>
<col width="100">
<col width="120">
<col width="120">
<col width="80">
<col width="400">
<col width="180">
<col width="100">
</colgroup>
<thead>
<tr>
<th>注册码</th>
<th>最大使用次数</th>
<th>已使用次数</th>
<th>状态</th>
<th>使用的用户</th>
<th>创建时间</th>
<th>创建用户</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for code in codes %}
<tr>
<td>{{ code.code }}</td>
<td>{{ code.all_cnt }}</td>
<td>{{ code.used_cnt }}</td>
<td>
{% if code.status == 1 %}
<i class="layui-icon layui-icon-ok-circle" style="color:#1E9FFF;"></i> 有效
{% elif code.status == 0 %}
<i class="layui-icon layui-icon-close-fill" style="color: #FF5722;"></i> 失效
{% endif %}
</td>
<td>
{% if code.user_list is None %}
暂无注册用户使用
{% else %}
{{ code.user_list }}
{% endif %}
</td>
<td>{{ code.create_time }}</td>
<td>{{ code.create_user }}</td>
<td>
<a href="javascript:void(0);" onclick="delRegisterCode('{{code.id}}')" class="layui-btn layui-btn-danger layui-btn-xs">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="layui-row">
<div class="pagination">
<span class="step-links">
{% if codes.has_previous %}
<a href="?page={{ codes.previous_page_number }}&kw={{codes.kw}}" class="layui-btn layui-btn-normal layui-btn-xs">上一页</a>
{% endif %}
<span class="current">
当前页: {{ codes.number }} 共 {{ codes.paginator.num_pages }} 页
</span>
{% if codes.has_next %}
<a href="?page={{ projects.next_page_number }}&kw={{projects.kw}}" class="layui-btn layui-btn-xs">下一页</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}
{% block custom_script %}
<script>
$.ajaxSetup({
data: {csrfmiddlewaretoken: '{{ csrf_token }}' },
});
//新增注册码
createRegisterCode = function(){
layer.open({
type:1,
title:'新增注册码',
area:'300px;',
id:'createRegCode',//配置ID
content:'<div style="margin:10px;"><input class="layui-input" id="regCodeCnt" type="number" value=1 placeholder="输入注册码最大使用次数"/></div>',
btn:['确定','取消'], //添加按钮
btnAlign:'c', //按钮居中
yes:function (index,layero) {
data = {
'types':1,
'all_cnt':$("#regCodeCnt").val(),
}
$.post("{% url 'register_code_manage' %}",data,function(r){
if(r.status){
//新增成功,刷新页面
window.location.reload();
//layer.close(index)
}else{
//新增失败,提示
console.log(r)
layer.msg(r.data)
}
})
},
});
}
//删除注册码
delRegisterCode = function(code_id){
layer.open({
type:1,
title:'删除文档',
area:'300px;',
id:'delRegCode',//配置ID
content:'<div style="margin-left:10px;">警告:此操作将删除此注册码!</div>',
btn:['确定','取消'], //添加按钮
btnAlign:'c', //按钮居中
yes:function (index,layero) {
data = {
'types':2,
'code_id':code_id,
}
$.post("{% url 'register_code_manage' %}",data,function(r){
if(r.status){
//删除成功,刷新页面
window.location.reload();
//layer.close(index)
}else{
//删除失败,提示
console.log(r)
layer.msg(r.data)
}
})
},
});
}
</script>
{% endblock %}

View File

@ -26,7 +26,15 @@
<div class="layui-input-inline">
<input type="checkbox" name="close_register" lay-skin="switch" lay-text="开启|关闭" {% if close_register %}checked{% endif %}>
</div>
<div class="layui-form-mid layui-word-aux">开启此选项将全站禁止新用户注册</div>
<div class="layui-form-mid layui-word-aux">开启此选项将禁止新用户注册</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">启用注册码</label>
<div class="layui-input-inline">
<input type="checkbox" name="enable_register_code" lay-skin="switch" lay-text="开启|关闭" {% if enable_register_code %}checked{% endif %}>
</div>
<div class="layui-form-mid layui-word-aux">开启此选项,新用户注册将需要填写注册码</div>
</div>
<div class="layui-form-item">

View File

@ -73,6 +73,7 @@
<script src="https://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<script src="{% static 'layui/layui.all.js' %}"></script>
<script>
var form = layui.form;
//为当前页面的目录链接添加样式
$("li a").each(function (i) {
var $me = $(this);

View File

@ -19,6 +19,20 @@
</div>
</form>
</div>
<div class="layui-row">
<form action="{% url 'manage_doc' %}" method="get" class="layui-form">
<div class="layui-form-item">
<label class="layui-form-label">状态筛选</label>
<div class="layui-input-block">
<input type="radio" name="doc_status" value="全部" title="全部({{all_cnt}}" checked>
<input type="radio" name="doc_status" value="已发布" title="已发布({{published_doc_cnt}}" disabled>
<input type="radio" name="doc_status" value="草稿" title="草稿({{draft_doc_cnt}}" disabled>
</div>
</div>
</form>
</div>
<div class="layui-row" lay-skin="line">
<table class="layui-table" id="doctemp-list" lay-skin="">
<thead>

View File

@ -63,6 +63,13 @@
<label class="doc-form-label">
<button class="layui-btn layui-btn-normal layui-btn-sm" onclick="createDoc()">发布文档</button>
</label>
<label class="doc-form-label">
{% if doc.status == 0 %}
<span>*当前状态:草稿</span>
{% elif doc.status == 1 %}
<span>*当前状态:已发布</span>
{% endif %}
</label>
</div>
</div>
@ -108,7 +115,7 @@
$.post("{% url 'modify_doc' doc_id=doc.id %}",data,function(r){
if(r.status){
//修改成功
layer.msg('保存成功,即将跳转',function(){
layer.msg('发布成功,即将跳转',function(){
window.location.href = "{% url 'doc' pro_id=doc.top_doc doc_id=doc.id %}";
});
}else{

View File

@ -52,6 +52,13 @@
<h2><strong>注册 - MrDoc</strong></h2>
</div>
<span style='color:red;margin-bottom: 10px;'>{{ errormsg }}</span>
{% if enable_register_code %}
<div class="layui-form-item">
<div class="layui-input-inline">
<input type="text" name="register_code" required lay-verify="required" placeholder="请输入注册码" autocomplete="off" class="layui-input">
</div>
</div>
{% endif %}
<div class="layui-form-item">
<div class="layui-input-inline">
<input type="text" name="username" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">