mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 15:38:15 +00:00
add admin operation logs page
This commit is contained in:
0
seahub/admin_log/__init__.py
Normal file
0
seahub/admin_log/__init__.py
Normal file
3
seahub/admin_log/admin.py
Normal file
3
seahub/admin_log/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
27
seahub/admin_log/migrations/0001_initial.py
Normal file
27
seahub/admin_log/migrations/0001_initial.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AdminLog',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('email', models.EmailField(max_length=254, db_index=True)),
|
||||
('operation', models.CharField(max_length=255, db_index=True)),
|
||||
('detail', models.TextField()),
|
||||
('datetime', models.DateTimeField(default=datetime.datetime.now)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-datetime'],
|
||||
},
|
||||
),
|
||||
]
|
0
seahub/admin_log/migrations/__init__.py
Normal file
0
seahub/admin_log/migrations/__init__.py
Normal file
74
seahub/admin_log/models.py
Normal file
74
seahub/admin_log/models.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2012-2017 Seafile Ltd.
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
|
||||
from seahub.admin_log.signals import admin_operation
|
||||
|
||||
ADMIN_LOG_OPERATION_TYPE = ('repo_transfer', 'repo_delete',
|
||||
'group_create', 'group_transfer', 'group_delete',
|
||||
'user_add', 'user_delete')
|
||||
|
||||
## operation: detail
|
||||
|
||||
# 'repo_transfer': {'id': repo_id, 'name': repo_name, 'from': from_user, 'to': to_user}
|
||||
# 'repo_delete': {'id': repo_id, 'name': repo_name, 'owner': repo_owner}
|
||||
|
||||
# 'group_create': {'id': group_id, 'name': group_name, 'owner': group_owner}
|
||||
# 'group_transfer': {'id': group_id, 'name': group_name, 'from': from_user, 'to': to_user}
|
||||
# 'group_delete': {'id': group_id, 'name': group_name, 'owner': group_owner}
|
||||
|
||||
# 'user_add': {'email': new_user}
|
||||
# 'user_delete': {'email': deleted_user}
|
||||
|
||||
|
||||
class AdminLogManager(models.Manager):
|
||||
|
||||
def add_admin_log(self, email, operation, detail):
|
||||
|
||||
model= super(AdminLogManager, self).create(
|
||||
email=email, operation=operation, detail=detail)
|
||||
|
||||
model.save()
|
||||
|
||||
return model
|
||||
|
||||
def get_admin_logs(self, email=None, operation=None):
|
||||
|
||||
logs = super(AdminLogManager, self).all()
|
||||
|
||||
if email and operation:
|
||||
filtered_logs = logs.filter(Q(email=email) & Q(operation = operation))
|
||||
elif email:
|
||||
filtered_logs = logs.filter(email=email)
|
||||
elif operation:
|
||||
filtered_logs = logs.filter(operation=operation)
|
||||
else:
|
||||
filtered_logs = logs
|
||||
|
||||
return filtered_logs
|
||||
|
||||
class AdminLog(models.Model):
|
||||
email = models.EmailField(db_index=True)
|
||||
operation = models.CharField(max_length=255, db_index=True)
|
||||
detail = models.CharField(max_length=255)
|
||||
datetime = models.DateTimeField(default=datetime.datetime.now)
|
||||
objects = AdminLogManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-datetime"]
|
||||
|
||||
|
||||
###### signal handlers
|
||||
@receiver(admin_operation)
|
||||
def admin_operation_cb(sender, **kwargs):
|
||||
admin_name = kwargs['admin_name']
|
||||
operation = kwargs['operation']
|
||||
detail = kwargs['detail']
|
||||
|
||||
detail_json = json.dumps(detail)
|
||||
AdminLog.objects.add_admin_log(admin_name,
|
||||
operation, detail_json)
|
4
seahub/admin_log/signals.py
Normal file
4
seahub/admin_log/signals.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2012-2017 Seafile Ltd.
|
||||
import django.dispatch
|
||||
|
||||
admin_operation = django.dispatch.Signal(providing_args=["admin_name", "operation", "detail"])
|
3
seahub/admin_log/views.py
Normal file
3
seahub/admin_log/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
@@ -16,7 +16,7 @@ from seahub.utils import is_valid_username
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||
from seahub.group.utils import is_group_member, is_group_admin, \
|
||||
validate_group_name, check_group_name_conflict
|
||||
|
||||
from seahub.admin_log.signals import admin_operation
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
@@ -129,15 +129,25 @@ class AdminGroups(APIView):
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
username = request.user.username
|
||||
new_owner = group_owner or username
|
||||
|
||||
# create group.
|
||||
try:
|
||||
group_id = ccnet_api.create_group(group_name, group_owner or username)
|
||||
group_id = ccnet_api.create_group(group_name, new_owner)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"id": group_id,
|
||||
"name": group_name,
|
||||
"owner": new_owner,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=username,
|
||||
operation='group_create', detail=admin_op_detail)
|
||||
|
||||
# get info of new group
|
||||
group_info = get_group_info(group_id)
|
||||
|
||||
@@ -198,16 +208,32 @@ class AdminGroup(APIView):
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
group_info = get_group_info(group_id)
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"id": group_id,
|
||||
"name": group.group_name,
|
||||
"from": old_owner,
|
||||
"to": new_owner,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='group_transfer', detail=admin_op_detail)
|
||||
|
||||
group_info = get_group_info(group_id)
|
||||
return Response(group_info)
|
||||
|
||||
def delete(self, request, group_id):
|
||||
""" Dismiss a specific group
|
||||
"""
|
||||
|
||||
try:
|
||||
group_id = int(group_id)
|
||||
group = ccnet_api.get_group(group_id)
|
||||
if not group:
|
||||
return Response({'success': True})
|
||||
|
||||
group_owner = group.creator_name
|
||||
group_name = group.group_name
|
||||
|
||||
try:
|
||||
ccnet_api.remove_group(group_id)
|
||||
seafile_api.remove_group_repos(group_id)
|
||||
except Exception as e:
|
||||
@@ -215,4 +241,13 @@ class AdminGroup(APIView):
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"id": group_id,
|
||||
"name": group_name,
|
||||
"owner": group_owner,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='group_delete', detail=admin_op_detail)
|
||||
|
||||
return Response({'success': True})
|
||||
|
@@ -17,6 +17,7 @@ from seahub.api2.utils import api_error
|
||||
from seahub.base.accounts import User
|
||||
from seahub.signals import repo_deleted
|
||||
from seahub.views import get_system_default_repo_id
|
||||
from seahub.admin_log.signals import admin_operation
|
||||
|
||||
try:
|
||||
from seahub.settings import MULTI_TENANCY
|
||||
@@ -150,24 +151,33 @@ class AdminLibrary(APIView):
|
||||
return api_error(status.HTTP_404_NOT_FOUND,
|
||||
'Library %s not found.' % repo_id)
|
||||
|
||||
repo_name = repo.name
|
||||
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||
if not repo_owner:
|
||||
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||
related_usernames = seaserv.get_related_users_by_repo(repo_id)
|
||||
|
||||
try:
|
||||
seafile_api.remove_repo(repo_id)
|
||||
repo_deleted.send(sender=None,
|
||||
org_id=-1,
|
||||
usernames=related_usernames,
|
||||
repo_owner=repo_owner,
|
||||
repo_id=repo_id,
|
||||
repo_name=repo.name)
|
||||
related_usernames = seaserv.get_related_users_by_repo(repo_id)
|
||||
|
||||
# send signal for seafevents
|
||||
repo_deleted.send(sender=None, org_id=-1, usernames=related_usernames,
|
||||
repo_owner=repo_owner, repo_id=repo_id, repo_name=repo.name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"id": repo_id,
|
||||
"name": repo_name,
|
||||
"owner": repo_owner,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='repo_delete', detail=admin_op_detail)
|
||||
|
||||
return Response({'success': True})
|
||||
|
||||
def put(self, request, repo_id, format=None):
|
||||
@@ -257,6 +267,16 @@ class AdminLibrary(APIView):
|
||||
|
||||
break
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"id": repo_id,
|
||||
"name": repo.name,
|
||||
"from": repo_owner,
|
||||
"to": new_owner,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='repo_transfer', detail=admin_op_detail)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
repo_info = get_repo_info(repo)
|
||||
|
||||
|
89
seahub/api2/endpoints/admin/logs.py
Normal file
89
seahub/api2/endpoints/admin/logs.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||
from seahub.admin_log.models import AdminLog, ADMIN_LOG_OPERATION_TYPE
|
||||
|
||||
from seahub.api2.permissions import IsProVersion
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.endpoints.admin.utils import generate_links_header_for_paginator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_log_info(log_obj):
|
||||
isoformat_timestr = datetime_to_isoformat_timestr(log_obj.datetime)
|
||||
log_info = {
|
||||
"email": log_obj.email,
|
||||
"operation": log_obj.operation,
|
||||
"detail": json.loads(log_obj.detail),
|
||||
"datetime": isoformat_timestr,
|
||||
}
|
||||
|
||||
return log_info
|
||||
|
||||
|
||||
class AdminLogs(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
permission_classes = (IsAdminUser, IsProVersion)
|
||||
|
||||
def get(self, request):
|
||||
""" List all logs
|
||||
|
||||
Permission checking:
|
||||
1. Admin user;
|
||||
"""
|
||||
|
||||
email = request.GET.get('email', '')
|
||||
operation = request.GET.get('operation', '')
|
||||
if operation:
|
||||
if operation not in ADMIN_LOG_OPERATION_TYPE:
|
||||
error_msg = 'operation invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
per_page = int(request.GET.get('per_page', '100'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
per_page = 100
|
||||
|
||||
if page <= 0:
|
||||
error_msg = 'page invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
if per_page <= 0:
|
||||
error_msg = 'per_page invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
# generate data result
|
||||
data = []
|
||||
offset = per_page * (page -1)
|
||||
total_count = AdminLog.objects.get_admin_logs(email=email, operation=operation).count()
|
||||
admin_logs = AdminLog.objects.get_admin_logs(email=email, operation=operation)[offset:offset+per_page]
|
||||
|
||||
for log in admin_logs:
|
||||
log_info = get_log_info(log)
|
||||
data.append(log_info)
|
||||
|
||||
result = {'data': data, 'total_count': total_count}
|
||||
resp = Response(result)
|
||||
|
||||
## generate `Links` header for paginator
|
||||
options_dict = {'email': email, 'operation': operation}
|
||||
base_url = reverse('api-v2.1-admin-admin-logs')
|
||||
links_header = generate_links_header_for_paginator(base_url,
|
||||
page, per_page, total_count, options_dict)
|
||||
resp['Links'] = links_header
|
||||
|
||||
return resp
|
@@ -2,6 +2,7 @@
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
import urllib
|
||||
|
||||
from seahub.utils import get_log_events_by_time
|
||||
|
||||
@@ -27,3 +28,50 @@ def get_log_events_by_type_and_time(log_type, start, end):
|
||||
events = get_log_events_by_time(log_type, start_timestamp, end_timestamp)
|
||||
events = events if events else []
|
||||
return events
|
||||
|
||||
def is_first_page(page):
|
||||
return True if page == 1 else False
|
||||
|
||||
def is_last_page(page, per_page, total_count):
|
||||
if page * per_page >= total_count:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def generate_links_header_for_paginator(base_url, page, per_page, total_count, option_dict={}):
|
||||
|
||||
if type(option_dict) is not dict:
|
||||
return ''
|
||||
|
||||
query_dict = {'page': 1, 'per_page': per_page}
|
||||
query_dict.update(option_dict)
|
||||
|
||||
# generate first page url
|
||||
first_page_url = base_url + '?' + urllib.urlencode(query_dict)
|
||||
|
||||
# generate last page url
|
||||
last_page_query_dict = {'page': (total_count / per_page) + 1}
|
||||
query_dict.update(last_page_query_dict)
|
||||
last_page_url = base_url + '?' + urllib.urlencode(query_dict)
|
||||
|
||||
# generate next page url
|
||||
next_page_query_dict = {'page': page + 1}
|
||||
query_dict.update(next_page_query_dict)
|
||||
next_page_url = base_url + '?' + urllib.urlencode(query_dict)
|
||||
|
||||
# generate prev page url
|
||||
prev_page_query_dict = {'page': page - 1}
|
||||
query_dict.update(prev_page_query_dict)
|
||||
prev_page_url = base_url + '?' + urllib.urlencode(query_dict)
|
||||
|
||||
# generate `Links` header
|
||||
links_header = ''
|
||||
if is_first_page(page):
|
||||
links_header = '<%s>; rel="next", <%s>; rel="last"' % (next_page_url, last_page_url)
|
||||
elif is_last_page(page, per_page, total_count):
|
||||
links_header = '<%s>; rel="first", <%s>; rel="prev"' % (first_page_url, prev_page_url)
|
||||
else:
|
||||
links_header = '<%s>; rel="next", <%s>; rel="last", <%s>; rel="first", <%s>; rel="prev"' % \
|
||||
(next_page_url, last_page_url, first_page_url, prev_page_url)
|
||||
|
||||
return links_header
|
||||
|
@@ -9,6 +9,8 @@ from django.conf import settings
|
||||
|
||||
from seaserv import check_permission, is_repo_owner, ccnet_api
|
||||
|
||||
from seahub.utils import is_pro_version
|
||||
|
||||
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
||||
|
||||
class IsRepoWritable(BasePermission):
|
||||
@@ -76,3 +78,11 @@ class CanGenerateUploadLink(BasePermission):
|
||||
"""
|
||||
def has_permission(self, request, *args, **kwargs):
|
||||
return request.user.permissions.can_generate_upload_link()
|
||||
|
||||
class IsProVersion(BasePermission):
|
||||
"""
|
||||
Check whether Seafile is pro version
|
||||
"""
|
||||
|
||||
def has_permission(self, request, *args, **kwargs):
|
||||
return is_pro_version()
|
||||
|
@@ -214,6 +214,7 @@ INSTALLED_APPS = (
|
||||
'seahub.help',
|
||||
'seahub.thumbnail',
|
||||
'seahub.password_session',
|
||||
'seahub.admin_log',
|
||||
)
|
||||
|
||||
# Enabled or disable constance(web settings).
|
||||
|
@@ -67,6 +67,10 @@
|
||||
<a href="{{ SITE_ROOT }}sys/termsadmin/"><span class="sf2-icon-wiki"></span>{% trans "Terms and Conditions" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="tab<% if (cur_tab == 'admin-logs') { %> tab-cur<% } %>">
|
||||
<a href="{{ SITE_ROOT }}sysadmin/#admin-logs/"><span class="sf2-icon-clock"></span>{% trans "Admin Logs" %}</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<% if (cur_tab == 'libraries') { %>
|
||||
<% if (option == 'all') { %>
|
||||
@@ -839,3 +843,75 @@
|
||||
<% } %>
|
||||
</td>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="admin-logs-tmpl">
|
||||
<div class="hd ovhd">
|
||||
<h3 class="fleft">{% trans "Admin Logs" %}</h3>
|
||||
</div>
|
||||
<span class="loading-icon loading-tip"></span>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">{% trans "Email" %}</th>
|
||||
<th width="15%">{% trans "Operation" %}</th>
|
||||
<th width="50%">{% trans "Detail" %}</th>
|
||||
<th width="15%">{% trans "Time" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="empty-tips hide">
|
||||
<h2 class="alc">{% trans "No Admin Logs" %}</h2>
|
||||
</div>
|
||||
<div id="paginator">
|
||||
<a class="prev js-previous hide" href="#">{% trans "Previous" %}</a>
|
||||
<a class="next js-next hide" href="#">{% trans "Next" %}</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="admin-log-item-tmpl">
|
||||
<td><a href="{{ SITE_ROOT }}useradmin/info/<% print(encodeURIComponent(email)); %>/"><%- email %></a></td>
|
||||
|
||||
<% if (operation=='repo_delete') { %>
|
||||
<td>{% trans "Delete Library" %}</td>
|
||||
<td>{% trans "Delete library" %} <strong title="<%- detail['id'] %>"><%- detail['name'] %></strong></td>
|
||||
<% } %>
|
||||
|
||||
<% if (operation=='repo_transfer') { %>
|
||||
<td>{% trans "Transfer Library" %}</td>
|
||||
<% if (app.pageOptions.is_pro && app.pageOptions.enable_sys_admin_view_repo) { %>
|
||||
<td>{% trans "Transfer library" %} <a href="#libs/<%- detail['id'] %>/"><%- detail['name'] %></a> {% trans "from" %} <a href="{{ SITE_ROOT }}useradmin/info/<%- detail['from'] %>/"><%- detail['from'] %></a> {% trans "to" %} <a href="{{ SITE_ROOT }}useradmin/info/<%- detail['to'] %>/"><%- detail['to'] %></a></td>
|
||||
<% } else { %>
|
||||
<td>{% trans "Transfer library" %} <%- detail['name'] %> {% trans "from" %} <a href="{{ SITE_ROOT }}useradmin/info/<%- detail['from'] %>/"><%- detail['from'] %></a> {% trans "to" %} <a href="{{ SITE_ROOT }}useradmin/info/<%- detail['to'] %>/"><%- detail['to'] %></a></td>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (operation=='group_create') { %>
|
||||
<td>{% trans "Create Group" %}</td>
|
||||
<td>{% trans "Create group" %} <span><a href="#groups/<%- detail['id'] %>/"><%- detail['name'] %></span></td>
|
||||
<% } %>
|
||||
|
||||
<% if (operation=='group_transfer') { %>
|
||||
<td>{% trans "Transfer Group" %}</td>
|
||||
|
||||
<td>{% trans "Transfer group" %} <a href="#groups/<%- detail['id'] %>/"><%- detail['name'] %></a> {% trans "from" %} <a href="{{ SITE_ROOT }}useradmin/info/<%- detail['from'] %>/"><%- detail['from'] %></a> {% trans "to" %} <a href="{{ SITE_ROOT }}useradmin/info/<%- detail['to'] %>/"><%- detail['to'] %></a></td>
|
||||
<% } %>
|
||||
|
||||
<% if (operation=='group_delete') { %>
|
||||
<td>{% trans "Delete Group" %}</td>
|
||||
<td>{% trans "Delete group" %} <strong title="<%- detail['id'] %>"><%- detail['name'] %></strong></td>
|
||||
<% } %>
|
||||
|
||||
<% if (operation=='user_add') { %>
|
||||
<td>{% trans "Add User" %}</td>
|
||||
<td>{% trans "Add user" %} <span><a href="{{ SITE_ROOT }}useradmin/info/<%- detail['email'] %>/"><%- detail['email'] %></a></span></td>
|
||||
<% } %>
|
||||
|
||||
<% if (operation=='user_delete') { %>
|
||||
<td>{% trans "Delete User" %}</td>
|
||||
<td>{% trans "Delete user" %} <strong><%- detail['email'] %></strong></td>
|
||||
<% } %>
|
||||
|
||||
<td><time title="<%- time %>"><%- time_from_now %></time></td>
|
||||
</script>
|
||||
|
@@ -73,6 +73,9 @@
|
||||
<a href="{{ SITE_ROOT }}sys/termsadmin/"><span class="sf2-icon-wiki"></span>{% trans "Terms and Conditions" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="tab">
|
||||
<a href="{{ SITE_ROOT }}sysadmin/#admin-logs/"><span class="sf2-icon-clock"></span>{% trans "Admin Logs" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
@@ -55,6 +55,7 @@ from seahub.api2.endpoints.admin.group_libraries import AdminGroupLibraries, Adm
|
||||
from seahub.api2.endpoints.admin.group_members import AdminGroupMembers, AdminGroupMember
|
||||
from seahub.api2.endpoints.admin.shares import AdminShares
|
||||
from seahub.api2.endpoints.admin.users_batch import AdminUsersBatch
|
||||
from seahub.api2.endpoints.admin.logs import AdminLogs
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
#from django.contrib import admin
|
||||
@@ -213,6 +214,8 @@ urlpatterns = patterns(
|
||||
url(r'^api/v2.1/admin/trash-libraries/$', AdminTrashLibraries.as_view(), name='api-v2.1-admin-trash-libraries'),
|
||||
url(r'^api/v2.1/admin/trash-libraries/(?P<repo_id>[-0-9a-f]{36})/$', AdminTrashLibrary.as_view(), name='api-v2.1-admin-trash-library'),
|
||||
url(r'^api/v2.1/admin/shares/$', AdminShares.as_view(), name='api-v2.1-admin-shares'),
|
||||
# TODO
|
||||
url(r'^api/v2.1/admin/admin-logs/$', AdminLogs.as_view(), name='api-v2.1-admin-admin-logs'),
|
||||
|
||||
url(r'^api/v2.1/admin/users/batch/$', AdminUsersBatch.as_view(), name='api-v2.1-admin-users-batch'),
|
||||
|
||||
|
@@ -54,11 +54,11 @@ from seahub.utils.user_permissions import (get_basic_user_roles,
|
||||
from seahub.views import get_system_default_repo_id
|
||||
from seahub.forms import SetUserQuotaForm, AddUserForm, BatchAddUserForm, \
|
||||
TermsAndConditionsForm
|
||||
from seahub.profile.forms import ProfileForm, DetailedProfileForm
|
||||
from seahub.options.models import UserOptions
|
||||
from seahub.profile.models import Profile, DetailedProfile
|
||||
from seahub.signals import repo_deleted
|
||||
from seahub.share.models import FileShare, UploadLinkShare
|
||||
from seahub.admin_log.signals import admin_operation
|
||||
import seahub.settings as settings
|
||||
from seahub.settings import INIT_PASSWD, SITE_NAME, SITE_ROOT, \
|
||||
SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, SEND_EMAIL_ON_RESETTING_USER_PASSWD, \
|
||||
@@ -712,6 +712,13 @@ def user_remove(request, email):
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, _(u'Failed to delete: the user does not exist'))
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"email": email,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='user_delete', detail=admin_op_detail)
|
||||
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
@login_required
|
||||
@@ -985,6 +992,13 @@ def user_add(request):
|
||||
err_msg = _(u'Fail to add user %s.') % email
|
||||
return HttpResponse(json.dumps({'error': err_msg}), status=403, content_type=content_type)
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"email": email,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='user_add', detail=admin_op_detail)
|
||||
|
||||
if user:
|
||||
User.objects.update_role(email, role)
|
||||
if config.FORCE_PASSWORD_CHANGE:
|
||||
@@ -1832,7 +1846,6 @@ def batch_add_user(request):
|
||||
Profile.objects.add_or_update(username, nickname, '')
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
try:
|
||||
department = row[3].strip() or ''
|
||||
@@ -1840,7 +1853,6 @@ def batch_add_user(request):
|
||||
DetailedProfile.objects.add_or_update(username, department, '')
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
try:
|
||||
role = row[4].strip() or ''
|
||||
@@ -1848,7 +1860,6 @@ def batch_add_user(request):
|
||||
User.objects.update_role(username, role)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
try:
|
||||
space_quota_mb = row[5].strip() or ''
|
||||
@@ -1858,7 +1869,6 @@ def batch_add_user(request):
|
||||
seafile_api.set_user_quota(username, space_quota)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
send_html_email_with_dj_template(
|
||||
username, dj_template='sysadmin/user_batch_add_email.html',
|
||||
@@ -1869,6 +1879,13 @@ def batch_add_user(request):
|
||||
'password': password,
|
||||
})
|
||||
|
||||
# send admin operation log signal
|
||||
admin_op_detail = {
|
||||
"email": username,
|
||||
}
|
||||
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||
operation='user_add', detail=admin_op_detail)
|
||||
|
||||
messages.success(request, _('Import succeeded'))
|
||||
else:
|
||||
messages.error(request, _(u'Please select a csv file first.'))
|
||||
|
@@ -179,6 +179,8 @@ define([
|
||||
case 'admin-trash-library': return siteRoot + 'api/v2.1/admin/trash-libraries/' + options.repo_id + '/';
|
||||
case 'admin_shares': return siteRoot + 'api/v2.1/admin/shares/';
|
||||
case 'sys_group_admin_export_excel': return siteRoot + 'sys/groupadmin/export-excel/';
|
||||
|
||||
case 'admin-logs': return siteRoot + 'api/v2.1/admin/admin-logs/';
|
||||
}
|
||||
},
|
||||
|
||||
|
40
static/scripts/sysadmin-app/collection/admin-logs.js
Normal file
40
static/scripts/sysadmin-app/collection/admin-logs.js
Normal file
@@ -0,0 +1,40 @@
|
||||
define([
|
||||
'underscore',
|
||||
'backbone.paginator',
|
||||
'common',
|
||||
'sysadmin-app/models/admin-log'
|
||||
], function(_, BackbonePaginator, Common, AdminLogModel) {
|
||||
'use strict';
|
||||
|
||||
var AdminLogCollection = Backbone.PageableCollection.extend({
|
||||
|
||||
model: AdminLogModel,
|
||||
|
||||
url: function() {
|
||||
return Common.getUrl({name: 'admin-logs'});
|
||||
},
|
||||
|
||||
state: {
|
||||
firstPage: 1,
|
||||
pageSize: 100,
|
||||
},
|
||||
|
||||
// Setting `null` as the value of any mapping
|
||||
// will remove it from the query string.
|
||||
queryParams: {
|
||||
currentPage: "page",
|
||||
pageSize: "per_page",
|
||||
totalPages: null,
|
||||
totalRecords: null,
|
||||
},
|
||||
|
||||
parseState: function (resp) {
|
||||
return {totalRecords: resp.total_count};
|
||||
},
|
||||
|
||||
parseRecords: function (resp) {
|
||||
return resp.data;
|
||||
}
|
||||
});
|
||||
return AdminLogCollection;
|
||||
});
|
11
static/scripts/sysadmin-app/models/admin-log.js
Normal file
11
static/scripts/sysadmin-app/models/admin-log.js
Normal file
@@ -0,0 +1,11 @@
|
||||
define([
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common',
|
||||
], function(_, Backbone, Common) {
|
||||
'use strict';
|
||||
|
||||
var AdminLogModel = Backbone.Model.extend({});
|
||||
|
||||
return AdminLogModel;
|
||||
});
|
@@ -18,12 +18,13 @@ define([
|
||||
'sysadmin-app/views/search-groups',
|
||||
'sysadmin-app/views/group-repos',
|
||||
'sysadmin-app/views/group-members',
|
||||
'sysadmin-app/views/admin-logs',
|
||||
'app/views/account'
|
||||
], function($, Backbone, Common, SideNavView, DashboardView,
|
||||
DesktopDevicesView, MobileDevicesView, DeviceErrorsView,
|
||||
ReposView, SearchReposView, SystemReposView, TrashReposView,
|
||||
SearchTrashReposView, DirView, GroupsView, SearchGroupsView,
|
||||
GroupReposView, GroupMembersView, AccountView) {
|
||||
GroupReposView, GroupMembersView, AdminLogsView, AccountView) {
|
||||
|
||||
"use strict";
|
||||
|
||||
@@ -42,8 +43,10 @@ define([
|
||||
'libs/:repo_id(/*path)': 'showLibraryDir',
|
||||
'groups/': 'showGroups',
|
||||
'search-groups/': 'showSearchGroups',
|
||||
'groups/:group_id/': 'showGroupLibraries',
|
||||
'groups/:group_id/libs/': 'showGroupLibraries',
|
||||
'groups/:group_id/members/': 'showGroupMembers',
|
||||
'admin-logs/': 'showAdminLogs',
|
||||
// Default
|
||||
'*actions': 'showDashboard'
|
||||
},
|
||||
@@ -76,6 +79,8 @@ define([
|
||||
this.groupReposView = new GroupReposView();
|
||||
this.groupMembersView = new GroupMembersView();
|
||||
|
||||
this.adminLogsView = new AdminLogsView();
|
||||
|
||||
app.ui.accountView = this.accountView = new AccountView();
|
||||
|
||||
this.currentView = this.dashboardView;
|
||||
@@ -223,6 +228,20 @@ define([
|
||||
this.switchCurrentView(this.groupMembersView);
|
||||
this.sideNavView.setCurTab('groups');
|
||||
this.groupMembersView.show(group_id);
|
||||
},
|
||||
|
||||
showAdminLogs: function() {
|
||||
var url = window.location.href;
|
||||
var page = url.match(/.*?page=(\d+)/);
|
||||
if (page) {
|
||||
var current_page = page[1];
|
||||
} else {
|
||||
var current_page = null;
|
||||
}
|
||||
|
||||
this.switchCurrentView(this.adminLogsView);
|
||||
this.sideNavView.setCurTab('admin-logs');
|
||||
this.adminLogsView.show({'current_page': current_page});
|
||||
}
|
||||
|
||||
});
|
||||
|
38
static/scripts/sysadmin-app/views/admin-log.js
Normal file
38
static/scripts/sysadmin-app/views/admin-log.js
Normal file
@@ -0,0 +1,38 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common',
|
||||
'moment',
|
||||
'app/views/widgets/hl-item-view'
|
||||
], function($, _, Backbone, Common, Moment, HLItemView) {
|
||||
'use strict';
|
||||
|
||||
var AdminLogView = HLItemView.extend({
|
||||
tagName: 'tr',
|
||||
|
||||
template: _.template($('#admin-log-item-tmpl').html()),
|
||||
|
||||
events: {
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
HLItemView.prototype.initialize.call(this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var data = this.model.toJSON(),
|
||||
created_at = Moment(data['datetime']);
|
||||
|
||||
data['time'] = created_at.format('LLLL');
|
||||
data['time_from_now'] = Common.getRelativeTimeStr(created_at);
|
||||
|
||||
this.$el.html(this.template(data));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return AdminLogView;
|
||||
});
|
159
static/scripts/sysadmin-app/views/admin-logs.js
Normal file
159
static/scripts/sysadmin-app/views/admin-logs.js
Normal file
@@ -0,0 +1,159 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common',
|
||||
'sysadmin-app/views/admin-log',
|
||||
'sysadmin-app/collection/admin-logs'
|
||||
], function($, _, Backbone, Common, AdminLogView, AdminLogCollection) {
|
||||
'use strict';
|
||||
|
||||
var AdminLogsView = Backbone.View.extend({
|
||||
|
||||
id: 'admin-logs',
|
||||
|
||||
template: _.template($("#admin-logs-tmpl").html()),
|
||||
|
||||
initialize: function() {
|
||||
this.adminLogCollection = new AdminLogCollection();
|
||||
this.listenTo(this.adminLogCollection, 'add', this.addOne);
|
||||
this.listenTo(this.adminLogCollection, 'reset', this.reset);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.append(this.template());
|
||||
|
||||
this.$table = this.$('table');
|
||||
this.$tableBody = $('tbody', this.$table);
|
||||
this.$loadingTip = this.$('.loading-tip');
|
||||
this.$emptyTip = this.$('.empty-tips');
|
||||
|
||||
this.$jsPrevious = this.$('.js-previous');
|
||||
this.$jsNext = this.$('.js-next');
|
||||
},
|
||||
|
||||
events: {
|
||||
'click #paginator .js-next': 'getNextPage',
|
||||
'click #paginator .js-previous': 'getPreviousPage'
|
||||
},
|
||||
|
||||
getNextPage: function() {
|
||||
this.initPage();
|
||||
this.adminLogCollection.getNextPage({reset: true});
|
||||
return false;
|
||||
},
|
||||
|
||||
getPreviousPage: function() {
|
||||
this.initPage();
|
||||
this.adminLogCollection.getPreviousPage({reset: true});
|
||||
return false;
|
||||
},
|
||||
|
||||
initPage: function() {
|
||||
this.$loadingTip.show();
|
||||
this.$table.hide();
|
||||
this.$tableBody.empty();
|
||||
this.$emptyTip.hide();
|
||||
|
||||
this.$jsNext.hide();
|
||||
this.$jsPrevious.hide();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.$el.detach();
|
||||
this.attached = false;
|
||||
},
|
||||
|
||||
show: function(option) {
|
||||
this.option = option;
|
||||
if (!this.attached) {
|
||||
this.attached = true;
|
||||
$("#right-panel").html(this.$el);
|
||||
}
|
||||
this.getContent();
|
||||
},
|
||||
|
||||
renderPaginator: function() {
|
||||
if (this.adminLogCollection.hasNextPage()) {
|
||||
this.$jsNext.show();
|
||||
} else {
|
||||
this.$jsNext.hide();
|
||||
}
|
||||
|
||||
if (this.adminLogCollection.hasPreviousPage()) {
|
||||
this.$jsPrevious.show();
|
||||
} else {
|
||||
this.$jsPrevious.hide();
|
||||
}
|
||||
},
|
||||
|
||||
getContent: function() {
|
||||
this.initPage();
|
||||
|
||||
var _this = this;
|
||||
var current_page = parseInt(this.option.current_page) || 1;
|
||||
var first_page = parseInt(this.adminLogCollection.state.firstPage);
|
||||
var total_page = parseInt(this.adminLogCollection.state.totalPages);
|
||||
|
||||
// `currentPage` must be firstPage <= currentPage < totalPages if 1-based.
|
||||
if (first_page <= current_page && current_page < total_page) {
|
||||
this.adminLogCollection.getPage(current_page).done(function () {
|
||||
_this.$loadingTip.hide();
|
||||
var current_page = _this.adminLogCollection.state.currentPage;
|
||||
app.router.navigate('admin-logs/?page=' + current_page);
|
||||
|
||||
if (_this.adminLogCollection.length > 0) {
|
||||
_this.$table.show();
|
||||
} else {
|
||||
_this.$emptyTip.show();
|
||||
}
|
||||
|
||||
_this.renderPaginator();
|
||||
});
|
||||
} else {
|
||||
// always get the first page if not use `getPage` method
|
||||
_this.adminLogCollection.state.currentPage = 1;
|
||||
this.adminLogCollection.fetch({
|
||||
cache: false,
|
||||
reset: true,
|
||||
error: function(collection, response, opts) {
|
||||
var err_msg;
|
||||
if (response.responseText) {
|
||||
if (response['status'] == 401 || response['status'] == 403) {
|
||||
err_msg = gettext("Permission error");
|
||||
} else {
|
||||
err_msg = $.parseJSON(response.responseText).error_msg;
|
||||
}
|
||||
} else {
|
||||
err_msg = gettext("Failed. Please check the network.");
|
||||
}
|
||||
Common.feedback(err_msg, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addOne: function(log) {
|
||||
var view = new AdminLogView({model: log});
|
||||
this.$tableBody.append(view.render().el);
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.$loadingTip.hide();
|
||||
|
||||
var current_page = this.adminLogCollection.state.currentPage;
|
||||
app.router.navigate('admin-logs/?page=' + current_page);
|
||||
|
||||
if (this.adminLogCollection.length > 0) {
|
||||
this.adminLogCollection.each(this.addOne, this);
|
||||
this.$table.show();
|
||||
} else {
|
||||
this.$emptyTip.show();
|
||||
}
|
||||
|
||||
this.renderPaginator();
|
||||
}
|
||||
});
|
||||
return AdminLogsView;
|
||||
});
|
Reference in New Issue
Block a user