diff --git a/oj/settings.py b/oj/settings.py index db9ea8f..793f605 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'account', + 'utils', 'rest_framework', 'rest_framework_swagger', diff --git a/oj/urls.py b/oj/urls.py index 19da589..0c8ea51 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -11,6 +11,8 @@ urlpatterns = [ url(r'^docs/', include('rest_framework_swagger.urls')), url(r'^admin/$', TemplateView.as_view(template_name="admin/index.html"), name="admin_index_page"), url(r'^login/$', TemplateView.as_view(template_name="oj/account/login.html"), name="user_login_page"), + url(r'^register/$', TemplateView.as_view(template_name="oj/account/register.html"), name="user_register_page"), + url(r'^change_password/$', TemplateView.as_view(template_name="oj/account/change_password.html"), name="user_change_password_page"), url(r'^api/login/$', UserLoginAPIView.as_view(), name="user_login_api"), url(r'^api/register/$', UserRegisterAPIView.as_view(), name="user_register_api"), url(r'^api/change_password/$', UserChangePasswordAPIView.as_view(), name="user_change_password_api"), diff --git a/requirements.txt b/requirements.txt index 00da332..bb0fce3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ django-redis-sessions djangorestframework django-rest-swagger celery -gunicorn \ No newline at end of file +gunicorn +coverage \ No newline at end of file diff --git a/static/src/js/app/oj/account/change_password.js b/static/src/js/app/oj/account/change_password.js new file mode 100644 index 0000000..e3e907f --- /dev/null +++ b/static/src/js/app/oj/account/change_password.js @@ -0,0 +1,69 @@ +require(["jquery", "bs_alert", "csrf", "validation"], function($, bs_alert, csrfHeader){ + $("#change_password-form").formValidation({ + framework: "bootstrap", + fields: { + username: { + validators: { + notEmpty: { + message: "请填写用户名" + } + } + }, + password: { + validators: { + notEmpty: { + message: "请填写旧密码" + } + } + }, + new_password: { + validators: { + notEmpty: { + message: "请填写新密码" + }, + stringLength: { + min: 6, + max: 30, + message: '密码长度必须在6到30位之间' + } + }, + onSuccess: function(e, data) { + data.fv.revalidateField('confirm_password'); + } + }, + confirm_password: { + validators: { + notEmpty: { + message: "请填写确认密码" + }, + confirm: { + original: $("#new_password"), + message: "两次输入的密码必须一致" + } + }, + } + } + } + ).on('success.form.fv', function(e) { + e.preventDefault(); + var username = $("#username").val(); + var new_password = $("#new_password ").val(); + var password = $("#password").val(); + $.ajax({ + beforeSend: csrfHeader, + url: "/api/change_password/", + data: {username: username, new_password: new_password , old_password : password}, + dataType: "json", + method: "post", + success: function (data) { + + if(!data.code){ + window.location.href="/login/"; + } + else{ + bs_alert(data.data); + } + } + }) + }); +}); \ No newline at end of file diff --git a/static/src/js/app/oj/account/login.js b/static/src/js/app/oj/account/login.js index b7c23e7..da4416d 100644 --- a/static/src/js/app/oj/account/login.js +++ b/static/src/js/app/oj/account/login.js @@ -1,4 +1,4 @@ -require(["jquery", "bs_alert", "validation"], function($, bs_alert){ +require(["jquery", "bs_alert", "csrf", "validation"], function($, bs_alert, csrfHeader){ $("#login-form") .formValidation({ framework: "bootstrap", @@ -24,6 +24,7 @@ require(["jquery", "bs_alert", "validation"], function($, bs_alert){ var username = $("#username").val(); var password = $("#password").val(); $.ajax({ + beforeSend: csrfHeader, url: "/api/login/", data: {username: username, password: password}, dataType: "json", diff --git a/static/src/js/app/oj/account/register.js b/static/src/js/app/oj/account/register.js new file mode 100644 index 0000000..e6b4deb --- /dev/null +++ b/static/src/js/app/oj/account/register.js @@ -0,0 +1,79 @@ +require(["jquery", "bs_alert", "csrf", "validation"], function($, bs_alert, csrfHeader){ + $("#register-form") + .formValidation({ + framework: "bootstrap", + fields: { + username: { + validators: { + notEmpty: { + message: "请填写用户名" + }, + stringLength: { + min: 3, + max: 30, + message: '用户名长度必须在3到30位之间' + }, + usernameCheck:{ + message: '用户名已存在' + } + } + }, + password: { + validators: { + notEmpty: { + message: "请填写密码" + }, + stringLength: { + min: 6, + max: 30, + message: '密码长度必须在6到30位之间' + } + }, + onSuccess: function(e, data) { + data.fv.revalidateField('confirm_password'); + } + }, + real_name: { + validators: { + notEmpty: { + message: "请填写真实姓名" + } + }, + }, + confirm_password: { + validators: { + notEmpty: { + message: "请填写确认密码" + }, + confirm: { + original: $("#password"), + message: "两次输入的密码必须一致" + } + } + } + } + } + ).on('success.form.fv', function(e) { + e.preventDefault(); + var username = $("#username").val(); + var real_name = $("#real_name").val(); + var password = $("#password").val(); + $.ajax({ + beforeSend: csrfHeader, + url: "/api/register/", + data: {username: username, real_name: real_name, password: password}, + dataType: "json", + method: "post", + success: function (data) { + if(!data.code){ + window.location.href="/login/"; + } + else{ + bs_alert(data.data); + } + } + + }) + }); + +}); \ No newline at end of file diff --git a/static/src/js/config.js b/static/src/js/config.js index dc17a5a..3907d1c 100644 --- a/static/src/js/config.js +++ b/static/src/js/config.js @@ -14,6 +14,7 @@ var require = { bs_alert: "utils/bs_alert", submit_code: "app/oj/problem/submit_code", contest: "app/admin/contest/contest", + csrf: "utils/csrf", //formValidation 不要在代码中单独使用,而是使用和修改utils/validation base: "lib/formValidation/base", @@ -25,7 +26,8 @@ var require = { "validator/date": "lib/formValidation/validator/date", "validator/integer": "lib/formValidation/validator/integer", "validator/between": "lib/formValidation/validator/between", - + 'validator/confirm':"lib/formValidation/validator/confirm", + "validator/usernameCheck":"lib/formValidation/validator/usernameCheck", //富文本编辑器 不要直接使用,而是使用上面的editor simditor: "lib/simditor/simditor", "simple-module": "lib/simditor/module", diff --git a/static/src/js/lib/formValidation/validator/confirm.js b/static/src/js/lib/formValidation/validator/confirm.js new file mode 100644 index 0000000..db5ad3a --- /dev/null +++ b/static/src/js/lib/formValidation/validator/confirm.js @@ -0,0 +1,32 @@ +/** + * confirm validator + */ + +(function(root, factory) { + + "use strict"; + + // AMD module is defined + if (typeof define === "function" && define.amd) { + define("validator/confirm", ["jquery", "base"], factory); + } else { + // planted over the root! + factory(root.jQuery, root.FormValidation); + } + +}(this, function ($, FormValidation) { + FormValidation.I18n = $.extend(true, FormValidation.I18n || {}, { + 'en_US': { + confirm: { + 'default': 'Please input the same value' + } + } + }); + FormValidation.Validator.confirm = { + validate: function(validator, $field, options) { + if (options.original.val() == $field.val() || $field.val()== '') + return true; + return false; + } + }; +})); diff --git a/static/src/js/lib/formValidation/validator/usernameCheck.js b/static/src/js/lib/formValidation/validator/usernameCheck.js new file mode 100644 index 0000000..36a7762 --- /dev/null +++ b/static/src/js/lib/formValidation/validator/usernameCheck.js @@ -0,0 +1,37 @@ +/** + * usernameCheck validator + */ +(function(root, factory) { + + "use strict"; + + // AMD module is defined + if (typeof define === "function" && define.amd) { + define("validator/usernameCheck", ["jquery", "base", "csrf"], factory); + } else { + // planted over the root! + factory(root.jQuery, root.FormValidation); + } +}(this, function ($, FormValidation, csrfHeader) { + FormValidation.I18n = $.extend(true, FormValidation.I18n || {}, { + 'en_US': { + usernameCheck: { + 'default': 'Please input the same value' + } + } + }); + FormValidation.Validator.usernameCheck = { + validate: function(validator, $field, options) { + if ($field.val() == '') + return true; + return !$.ajax({ + async: false, + beforeSend: csrfHeader, + url: "/api/username_check/", + data: {username: $field.val()}, + dataType: "json", + method: "post", + }).responseJSON.data; + } + }; +})); diff --git a/static/src/js/utils/csrf.js b/static/src/js/utils/csrf.js new file mode 100644 index 0000000..767542b --- /dev/null +++ b/static/src/js/utils/csrf.js @@ -0,0 +1,16 @@ +define("csrf",function(){ + function get_cookie(cookie_name) { + var name = cookie_name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1); + if (c.indexOf(name) != -1) return c.substring(name.length, c.length); + } + return ""; + } + function csrfHeader(xhr){ + xhr.setRequestHeader("X-CSRFToken", get_cookie("csrftoken")); + } + return csrfHeader; +}); diff --git a/static/src/js/utils/validation.js b/static/src/js/utils/validation.js index ab3c2e7..5fe792f 100644 --- a/static/src/js/utils/validation.js +++ b/static/src/js/utils/validation.js @@ -7,5 +7,8 @@ define("validation", 'validator/stringLength', 'validator/date', 'validator/integer', - 'validator/between'], function () { + 'validator/between', + 'validator/confirm', + 'validator/usernameCheck'], + function () { }); \ No newline at end of file diff --git a/template/oj/account/change_password.html b/template/oj/account/change_password.html index 71ce692..65de447 100644 --- a/template/oj/account/change_password.html +++ b/template/oj/account/change_password.html @@ -1,10 +1,34 @@ - - - - - - - +{% extends "oj_base.html" %} +{% block body %} +
+
+

修改密码

- - \ No newline at end of file +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+{% endblock %} +{% block js_block %} + +{% endblock %} \ No newline at end of file diff --git a/template/oj/account/register.html b/template/oj/account/register.html index 71ce692..a93014c 100644 --- a/template/oj/account/register.html +++ b/template/oj/account/register.html @@ -1,10 +1,34 @@ - - - - - - - +{% extends "oj_base.html" %} +{% block body %} +
+
+

用户注册

- - \ No newline at end of file +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+{% endblock %} +{% block js_block %} + +{% endblock %} \ No newline at end of file diff --git a/tools/runserver.cmd b/tools/runserver.cmd new file mode 100644 index 0000000..b458e55 --- /dev/null +++ b/tools/runserver.cmd @@ -0,0 +1,5 @@ +@echo off +python manage.py runserver +cls +cd.. +python manage.py runserver \ No newline at end of file diff --git a/tools/runserver.sh b/tools/runserver.sh new file mode 100644 index 0000000..d508cfc --- /dev/null +++ b/tools/runserver.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +python manage.py runserver \ No newline at end of file diff --git a/tools/runtest.cmd b/tools/runtest.cmd new file mode 100644 index 0000000..e6d9ad7 --- /dev/null +++ b/tools/runtest.cmd @@ -0,0 +1,12 @@ +@echo off +coverage run --source='.' manage.py test +coverage html +cd htmlcov +index.html +cls +cd.. +coverage run --source='.' manage.py test +coverage html +cd htmlcov +index.html + diff --git a/tools/runtest.sh b/tools/runtest.sh new file mode 100644 index 0000000..f41a02b --- /dev/null +++ b/tools/runtest.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +coverage run --source='.' manage.py test +test_result=$? +if [ "$test_result" -eq 0 ];then + coverage html + open htmlcov/index.html +fi diff --git a/utils/shortcuts.py b/utils/shortcuts.py index bd5f286..852622f 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,4 +1,6 @@ # coding=utf-8 +from django.core.paginator import Paginator + from rest_framework.response import Response @@ -11,4 +13,73 @@ def serializer_invalid_response(serializer): def success_response(data): - return Response(data={"code": 0, "data": data}) \ No newline at end of file + return Response(data={"code": 0, "data": data}) + + +def paginate(request, query_set, object_serializer): + """ + 用于分页的函数 + 如果 url 里面不含有paging=true,那么将返回全部数据。类似 + [ + { + "username": "1111111", + "password": "123456" + } + ] + 如果 url 中有 paging=true 的参数, + 然后还需要读取其余的两个参数,page=[int],需要的页码,p + age_size=[int],一页的数据条数 + 参数错误的时候,返回{"code": 1, "data": u"参数错误"} + 返回的数据格式 + { + "code": 0, + "data": { + "previous_page": null, + "results": [ + { + "username": "1111111", + "password": "123456" + } + ], + "next_page": 2 + } + } + :param query_set 数据库查询结果 + :param object_serializer: 序列化单个object的serializer + :return response + """ + need_paginate = request.GET.get("paging", None) + # 如果请求的参数里面没有paging=true的话 就返回全部数据 + if need_paginate != "true": + return success_response(data=object_serializer(query_set, many=True).data) + + page_size = request.GET.get("page_size", None) + if not page_size: + return error_response(u"参数错误") + + try: + page_size = int(page_size) + except Exception: + return error_response(u"参数错误") + + paginator = Paginator(query_set, page_size) + page = request.GET.get("page", None) + + try: + current_page = paginator.page(page) + except Exception: + return error_response(u"参数错误") + + data = {"results": object_serializer(current_page, many=True).data, "previous_page": None, "next_page": None} + + try: + data["previous_page"] = current_page.previous_page_number() + except Exception: + pass + + try: + data["next_page"] = current_page.next_page_number() + except Exception: + pass + + return success_response(data) \ No newline at end of file diff --git a/utils/test_urls.py b/utils/test_urls.py new file mode 100644 index 0000000..0500d16 --- /dev/null +++ b/utils/test_urls.py @@ -0,0 +1,9 @@ +# coding=utf-8 +from django.conf.urls import include, url + + + +urlpatterns = [ + url(r'^paginate_test/$', "utils.tests.pagination_test_func"), +] + diff --git a/utils/tests.py b/utils/tests.py new file mode 100644 index 0000000..519870c --- /dev/null +++ b/utils/tests.py @@ -0,0 +1,64 @@ +# coding=utf-8 +from rest_framework.test import APIClient, APITestCase +from rest_framework import serializers +from rest_framework.decorators import api_view + +from account.models import User +from .shortcuts import paginate + + +class PaginationTestSerialiser(serializers.Serializer): + username = serializers.CharField(max_length=100) + + +@api_view(["GET"]) +def pagination_test_func(request): + return paginate(request, User.objects.all(), PaginationTestSerialiser) + + +class PaginatorTest(APITestCase): + urls = "utils.test_urls" + + def setUp(self): + self.client = APIClient() + self.url = "/paginate_test/" + User.objects.create(username="test1") + User.objects.create(username="test2") + + def test_no_paginate(self): + response = self.client.get(self.url) + self.assertEqual(response.data["code"], 0) + self.assertNotIn("next_page", response.data["data"]) + self.assertNotIn("previous_page", response.data["data"]) + + def test_error_parameter(self): + response = self.client.get(self.url + "?paging=true") + self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) + + response = self.client.get(self.url + "?paging=true&page_size=-1") + self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) + + response = self.client.get(self.url + "?paging=true&page_size=aa") + self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) + + response = self.client.get(self.url + "?paging=true&page_size=1&page=-1") + self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) + + response = self.client.get(self.url + "?paging=true&page_size=aaa&page=1") + self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) + + response = self.client.get(self.url + "?paging=true&page_size=1&page=aaa") + self.assertEqual(response.data, {"code": 1, "data": u"参数错误"}) + + def test_correct_paginate(self): + response = self.client.get(self.url + "?paging=true&limit=1&page_size=1&page=1") + self.assertEqual(response.data["code"], 0) + self.assertEqual(response.data["data"]["previous_page"], None) + self.assertEqual(response.data["data"]["next_page"], 2) + self.assertEqual(len(response.data["data"]["results"]), 1) + + response = self.client.get(self.url + "?paging=true&limit=1&page_size=2&page=1") + self.assertEqual(response.data["code"], 0) + self.assertEqual(response.data["data"]["previous_page"], None) + self.assertEqual(response.data["data"]["next_page"], None) + self.assertEqual(len(response.data["data"]["results"]), 2)