Accept Merge Request #127 小BUG修复 : (dev-sxw -> dev)
Merge Request: 小BUG修复 Created By: @esp Reviewed By: @virusdefender Accepted By: @virusdefender URL: https://coding.net/u/virusdefender/p/qduoj/git/merge/127?tab=files
This commit is contained in:
@@ -36,7 +36,6 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "date
|
|||||||
ajaxData.groups.push(parseInt(vm.choseGroupList[i].id))
|
ajaxData.groups.push(parseInt(vm.choseGroupList[i].id))
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(ajaxData);
|
|
||||||
$.ajax({ // Add contest
|
$.ajax({ // Add contest
|
||||||
beforeSend: csrfTokenHeader,
|
beforeSend: csrfTokenHeader,
|
||||||
url: "/api/admin/contest/",
|
url: "/api/admin/contest/",
|
||||||
@@ -64,11 +63,9 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "date
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
bsAlert(data.data);
|
bsAlert(data.data);
|
||||||
console.log(data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(ajaxData));
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -21,6 +21,10 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
bsAlert("你没有选择参赛用户!");
|
bsAlert("你没有选择参赛用户!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (vm.editDescription == "") {
|
||||||
|
bsAlert("比赛描述不能为空!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (vm.choseGroupList[0].id == 0) { //everyone | public contest
|
if (vm.choseGroupList[0].id == 0) { //everyone | public contest
|
||||||
if (vm.editPassword) {
|
if (vm.editPassword) {
|
||||||
ajaxData.password = vm.editPassword;
|
ajaxData.password = vm.editPassword;
|
||||||
@@ -36,29 +40,26 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
ajaxData.groups.push(parseInt(vm.choseGroupList[i].id))
|
ajaxData.groups.push(parseInt(vm.choseGroupList[i].id))
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(ajaxData);
|
$.ajax({ // Add contest
|
||||||
$.ajax({ // Add contest
|
beforeSend: csrfTokenHeader,
|
||||||
beforeSend: csrfTokenHeader,
|
url: "/api/admin/contest/",
|
||||||
url: "/api/admin/contest/",
|
dataType: "json",
|
||||||
dataType: "json",
|
contentType: "application/json",
|
||||||
contentType: "application/json",
|
data: JSON.stringify(ajaxData),
|
||||||
data: JSON.stringify(ajaxData),
|
method: "put",
|
||||||
method: "put",
|
contentType: "application/json",
|
||||||
contentType: "application/json",
|
success: function (data) {
|
||||||
success: function (data) {
|
if (!data.code) {
|
||||||
if (!data.code) {
|
bsAlert("修改成功!");
|
||||||
bsAlert("修改成功!");
|
vm.editingContestId = 0; // Hide the editor
|
||||||
console.log(data);
|
vm.getPage(1); // Refresh the contest list
|
||||||
vm.getPage(1);
|
}
|
||||||
}
|
else {
|
||||||
else {
|
bsAlert(data.data);
|
||||||
bsAlert(data.data);
|
}
|
||||||
console.log(data);
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
console.log(JSON.stringify(ajaxData));
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
vm.editChoseGroupList= [];
|
vm.editChoseGroupList= [];
|
||||||
vm.editingProblemContestIndex= 0;
|
vm.editingProblemContestIndex= 0;
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
var vm = avalon.define({
|
var vm = avalon.define({
|
||||||
$id: "contestList",
|
$id: "contestList",
|
||||||
contestList: [],
|
contestList: [],
|
||||||
@@ -98,6 +99,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
nextPage: 0,
|
nextPage: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPage: 1,
|
totalPage: 1,
|
||||||
|
showVisibleOnly: false,
|
||||||
group: "-1",
|
group: "-1",
|
||||||
groupList: [],
|
groupList: [],
|
||||||
choseGroupList: [],
|
choseGroupList: [],
|
||||||
@@ -139,6 +141,8 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
getPageData(page_index);
|
getPageData(page_index);
|
||||||
},
|
},
|
||||||
showEditContestArea: function (contestId) {
|
showEditContestArea: function (contestId) {
|
||||||
|
if (vm.editingContestId && !confirm("如果继续将丢失未保存的信息,是否继续?"))
|
||||||
|
return;
|
||||||
if (contestId == vm.editingContestId)
|
if (contestId == vm.editingContestId)
|
||||||
vm.editingContestId = 0;
|
vm.editingContestId = 0;
|
||||||
else {
|
else {
|
||||||
@@ -148,15 +152,11 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
vm.editStartTime = vm.contestList[contestId-1].start_time.substring(0,16).replace("T"," ");
|
vm.editStartTime = vm.contestList[contestId-1].start_time.substring(0,16).replace("T"," ");
|
||||||
vm.editEndTime = vm.contestList[contestId-1].end_time.substring(0,16).replace("T"," ");
|
vm.editEndTime = vm.contestList[contestId-1].end_time.substring(0,16).replace("T"," ");
|
||||||
vm.editMode = vm.contestList[contestId-1].mode;
|
vm.editMode = vm.contestList[contestId-1].mode;
|
||||||
editVisible = vm.contestList[contestId-1].visible;
|
vm.editVisible = vm.contestList[contestId-1].visible;
|
||||||
if (vm.contestList[contestId-1].contest_type == 0) { //contest type == 0, contest in group
|
if (vm.contestList[contestId-1].contest_type == 0) { //contest type == 0, contest in group
|
||||||
//Clear the choseGroupList
|
//Clear the choseGroupList
|
||||||
var stack = [], sp;
|
while (vm.choseGroupList.length) {
|
||||||
for (sp = 0; sp < vm.choseGroupList.length; sp++){
|
vm.removeGroup(0);
|
||||||
stack. push(vm.choseGroupList[sp].index);
|
|
||||||
}
|
|
||||||
while (sp--){
|
|
||||||
vm.removeGroup(stack[sp]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < vm.contestList[contestId-1].groups.length; i++){
|
for (var i = 0; i < vm.contestList[contestId-1].groups.length; i++){
|
||||||
@@ -215,8 +215,8 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
if (vm.groupList[vm.group].id == 0){
|
if (vm.groupList[vm.group].id == 0){
|
||||||
vm.passwordUsable = true;
|
vm.passwordUsable = true;
|
||||||
vm.choseGroupList = [];
|
vm.choseGroupList = [];
|
||||||
for (var key in vm.groupList){
|
for (var i = 0; i < vm.groupList.length; i++) {
|
||||||
vm.groupList[key].chose = true;
|
vm.groupList[i].chose = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vm.groupList[vm.group]. chose = true;
|
vm.groupList[vm.group]. chose = true;
|
||||||
@@ -227,8 +227,8 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
removeGroup: function(groupIndex){
|
removeGroup: function(groupIndex){
|
||||||
if (vm.groupList[vm.choseGroupList[groupIndex].index].id == 0){
|
if (vm.groupList[vm.choseGroupList[groupIndex].index].id == 0){
|
||||||
vm.passwordUsable = false;
|
vm.passwordUsable = false;
|
||||||
for (key in vm.groupList){
|
for (var i = 0; i < vm.groupList.length; i++) {
|
||||||
vm.groupList[key].chose = false;
|
vm.groupList[i].chose = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vm.groupList[vm.choseGroupList[groupIndex].index].chose = false;
|
vm.groupList[vm.choseGroupList[groupIndex].index].chose = false;
|
||||||
@@ -238,7 +238,6 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
vm.$fire("up!showContestProblemPage", 0, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode);
|
vm.$fire("up!showContestProblemPage", 0, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode);
|
||||||
},
|
},
|
||||||
showProblemEditor: function(el) {
|
showProblemEditor: function(el) {
|
||||||
console.log(el);
|
|
||||||
vm.$fire("up!showContestProblemPage", el.id, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode);
|
vm.$fire("up!showContestProblemPage", el.id, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode);
|
||||||
},
|
},
|
||||||
getYesOrNo: function(yORn) {
|
getYesOrNo: function(yORn) {
|
||||||
@@ -246,6 +245,10 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
return "否";
|
return "否";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
vm.$watch("showVisibleOnly", function() {
|
||||||
|
getPageData(1);
|
||||||
|
})
|
||||||
|
}
|
||||||
getPageData(1);
|
getPageData(1);
|
||||||
|
|
||||||
//init time picker
|
//init time picker
|
||||||
@@ -264,6 +267,8 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
|
|||||||
|
|
||||||
function getPageData(page) {
|
function getPageData(page) {
|
||||||
var url = "/api/admin/contest/?paging=true&page=" + page + "&page_size=10";
|
var url = "/api/admin/contest/?paging=true&page=" + page + "&page_size=10";
|
||||||
|
if (vm.showVisibleOnly)
|
||||||
|
url += "&visible=true"
|
||||||
if (vm.keyword != "")
|
if (vm.keyword != "")
|
||||||
url += "&keyword=" + vm.keyword;
|
url += "&keyword=" + vm.keyword;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -158,7 +158,6 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var problem = data.data;
|
var problem = data.data;
|
||||||
console.log(problem);
|
|
||||||
vm.title = problem.title;
|
vm.title = problem.title;
|
||||||
vm.description = problem.description;
|
vm.description = problem.description;
|
||||||
vm.timeLimit = problem.time_limit;
|
vm.timeLimit = problem.time_limit;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
require(["jquery", "avalon", "csrfToken", "bsAlert", "formValidation"], function ($, avalon, csrfTokenHeader, bsAlert) {
|
require(["jquery", "avalon", "csrfToken", "bsAlert"], function ($, avalon, csrfTokenHeader, bsAlert) {
|
||||||
|
|
||||||
avalon.ready(function () {
|
avalon.ready(function () {
|
||||||
avalon.vmodels.submissionList = null;
|
avalon.vmodels.submissionList = null;
|
||||||
@@ -41,9 +41,11 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "formValidation"], function
|
|||||||
getPage: function (page_index) {
|
getPage: function (page_index) {
|
||||||
getPageData(page_index);
|
getPageData(page_index);
|
||||||
},
|
},
|
||||||
|
|
||||||
showSubmissionDetailPage: function (submissionId) {
|
showSubmissionDetailPage: function (submissionId) {
|
||||||
|
|
||||||
|
},
|
||||||
|
showProblemListPage: function(){
|
||||||
|
vm.$fire("up!showProblemListPage");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3,58 +3,68 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "validator"], function ($,
|
|||||||
|
|
||||||
// avalon:定义模式 userList
|
// avalon:定义模式 userList
|
||||||
avalon.ready(function () {
|
avalon.ready(function () {
|
||||||
avalon.vmodels.userList = null;
|
//avalon.vmodels.userList = null;
|
||||||
var vm = avalon.define({
|
if (avalon.vmodels.userList) {
|
||||||
$id: "userList",
|
var vm = avalon.vmodels.userList;
|
||||||
//通用变量
|
// initialize avalon object
|
||||||
userList: [],
|
userList = []; previousPage= 0; nextPage= 0; page = 1;
|
||||||
previousPage: 0,
|
editingUserId= 0; totalPage = 1; keyword= ""; showAdminOnly= false;
|
||||||
nextPage: 0,
|
//user editor fields
|
||||||
page: 1,
|
username= ""; realName= ""; email= ""; adminType= 0; id= 0;
|
||||||
editingUserId: 0,
|
}
|
||||||
totalPage: 1,
|
else {
|
||||||
userType: ["一般用户", "管理员", "超级管理员"],
|
var vm = avalon.define({
|
||||||
keyword: "",
|
$id: "userList",
|
||||||
showAdminOnly: false,
|
//通用变量
|
||||||
//编辑区域同步变量
|
userList: [],
|
||||||
username: "",
|
previousPage: 0,
|
||||||
realName: "",
|
nextPage: 0,
|
||||||
email: "",
|
page: 1,
|
||||||
adminType: 0,
|
editingUserId: 0,
|
||||||
id: 0,
|
totalPage: 1,
|
||||||
getNext: function () {
|
userType: ["一般用户", "管理员", "超级管理员"],
|
||||||
if (!vm.nextPage)
|
keyword: "",
|
||||||
return;
|
showAdminOnly: false,
|
||||||
getPageData(vm.page + 1);
|
//编辑区域同步变量
|
||||||
},
|
username: "",
|
||||||
getPrevious: function () {
|
realName: "",
|
||||||
if (!vm.previousPage)
|
email: "",
|
||||||
return;
|
adminType: 0,
|
||||||
getPageData(vm.page - 1);
|
id: 0,
|
||||||
},
|
getNext: function () {
|
||||||
getBtnClass: function (btn) { //上一页/下一页按钮启用禁用逻辑
|
if (!vm.nextPage)
|
||||||
if (btn) {
|
return;
|
||||||
return vm.nextPage ? "btn btn-primary" : "btn btn-primary disabled";
|
getPageData(vm.page + 1);
|
||||||
|
},
|
||||||
|
getPrevious: function () {
|
||||||
|
if (!vm.previousPage)
|
||||||
|
return;
|
||||||
|
getPageData(vm.page - 1);
|
||||||
|
},
|
||||||
|
getBtnClass: function (btn) { //上一页/下一页按钮启用禁用逻辑
|
||||||
|
if (btn == "next") {
|
||||||
|
return vm.nextPage ? "btn btn-primary" : "btn btn-primary disabled";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return vm.previousPage ? "btn btn-primary" : "btn btn-primary disabled";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editUser: function (user) { //点击编辑按钮的事件,显示/隐藏编辑区
|
||||||
|
vm.username = user.username;
|
||||||
|
vm.realName = user.real_name;
|
||||||
|
vm.adminType = user.admin_type;
|
||||||
|
vm.email = user.email;
|
||||||
|
vm.id = user.id;
|
||||||
|
if (vm.editingUserId == user.id)
|
||||||
|
vm.editingUserId = 0;
|
||||||
|
else
|
||||||
|
vm.editingUserId = user.id;
|
||||||
|
},
|
||||||
|
search: function () {
|
||||||
|
getPageData(1);
|
||||||
}
|
}
|
||||||
else {
|
});
|
||||||
return vm.previousPage ? "btn btn-primary" : "btn btn-primary disabled";
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
editUser: function (user) { //点击编辑按钮的事件,显示/隐藏编辑区
|
|
||||||
vm.username = user.username;
|
|
||||||
vm.realName = user.real_name;
|
|
||||||
vm.adminType = user.admin_type;
|
|
||||||
vm.email = user.email;
|
|
||||||
vm.id = user.id;
|
|
||||||
if (vm.editingUserId == user.id)
|
|
||||||
vm.editingUserId = 0;
|
|
||||||
else
|
|
||||||
vm.editingUserId = user.id;
|
|
||||||
},
|
|
||||||
search: function () {
|
|
||||||
getPageData(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
vm.$watch("showAdminOnly", function () {
|
vm.$watch("showAdminOnly", function () {
|
||||||
getPageData(1);
|
getPageData(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class ContestSubmissionAPIView(APIView):
|
|||||||
"""
|
"""
|
||||||
创建比赛的提交
|
创建比赛的提交
|
||||||
---
|
---
|
||||||
request_serializer: ConestSubmissionSerializer
|
request_serializer: CreateContestSubmissionSerializer
|
||||||
"""
|
"""
|
||||||
serializer = CreateContestSubmissionSerializer(data=request.data)
|
serializer = CreateContestSubmissionSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
|||||||
@@ -103,4 +103,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/app/admin/contest/add_contest.js"></script>
|
<script src="/static/js/app/admin/contest/addContest.js"></script>
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>仅显示可见 <input ms-duplex-checked="showVisibleOnly" type="checkbox"/></label>
|
||||||
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
页数:{{ page }}/{{ totalPage }}
|
页数:{{ page }}/{{ totalPage }}
|
||||||
<button ms-attr-class="getBtnClass('pre')" ms-click="getPrevious">上一页</button>
|
<button ms-attr-class="getBtnClass('pre')" ms-click="getPrevious">上一页</button>
|
||||||
@@ -161,6 +164,7 @@
|
|||||||
<th>编号</th>
|
<th>编号</th>
|
||||||
<th>题目</th>
|
<th>题目</th>
|
||||||
<th ms-visible="editMode=='2'">分值</th>
|
<th ms-visible="editMode=='2'">分值</th>
|
||||||
|
<th>可见</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -168,6 +172,7 @@
|
|||||||
<td>{{ el.sort_index }}</td>
|
<td>{{ el.sort_index }}</td>
|
||||||
<td>{{ el.title }}</td>
|
<td>{{ el.title }}</td>
|
||||||
<td ms-visible="editMode=='2'">{{ el.score}}</td>
|
<td ms-visible="editMode=='2'">{{ el.score}}</td>
|
||||||
|
<td>{{ getYesOrNo(el.visible) }}</td>
|
||||||
<td>{{ el.create_time|date("yyyy-MM-dd HH:mm:ss") }}</td>
|
<td>{{ el.create_time|date("yyyy-MM-dd HH:mm:ss") }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="javascript:void(0)" class="btn-sm btn-info"
|
<a href="javascript:void(0)" class="btn-sm btn-info"
|
||||||
@@ -180,4 +185,4 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/app/admin/contest/contest_list.js"></script>
|
<script src="/static/js/app/admin/contest/contestList.js"></script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<form id="edit-problem-form">
|
<form id="edit-problem-form">
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pager">
|
<ul class="pager">
|
||||||
<li class="previous" ms-click="goBack()"><a href="javascript:void(0)"><span
|
<li class="previous" ms-click="goBack(0)"><a href="javascript:void(0)"><span
|
||||||
aria-hidden="true">←</span> 返回</a></li>
|
aria-hidden="true">←</span> 返回</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -135,4 +135,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/app/admin/contest/edit_problem.js"></script>
|
<script src="/static/js/app/admin/contest/editProblem.js"></script>
|
||||||
|
|||||||
@@ -134,4 +134,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/app/admin/problem/add_problem.js"></script>
|
<script src="/static/js/app/admin/problem/addProblem.js"></script>
|
||||||
@@ -140,4 +140,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/app/admin/problem/edit_problem.js"></script>
|
<script src="/static/js/app/admin/problem/editProblem.js"></script>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<div ms-controller="submissionList" class="col-md-9">
|
<div ms-controller="submissionList" class="col-md-9">
|
||||||
|
<nav>
|
||||||
|
<ul class="pager">
|
||||||
|
<li class="previous" ms-click="showProblemListPage()"><a href="javascript:void(0)"><span
|
||||||
|
aria-hidden="true">←</span> 返回</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
<h1>提交列表</h1>
|
<h1>提交列表</h1>
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@@ -25,4 +31,4 @@
|
|||||||
<button ms-attr-class="getBtnClass('next')" ms-click="getNext">下一页</button>
|
<button ms-attr-class="getBtnClass('next')" ms-click="getNext">下一页</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/app/admin/problem/submission_list.js"></script>
|
<script src="/static/js/app/admin/problem/submissionList.js"></script>
|
||||||
Reference in New Issue
Block a user