diff --git a/seahub/admin_log/__init__.py b/seahub/admin_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/admin_log/admin.py b/seahub/admin_log/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/seahub/admin_log/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/seahub/admin_log/migrations/0001_initial.py b/seahub/admin_log/migrations/0001_initial.py new file mode 100644 index 0000000000..60f27e16a4 --- /dev/null +++ b/seahub/admin_log/migrations/0001_initial.py @@ -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'], + }, + ), + ] diff --git a/seahub/admin_log/migrations/__init__.py b/seahub/admin_log/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/admin_log/models.py b/seahub/admin_log/models.py new file mode 100644 index 0000000000..1ccf4d4906 --- /dev/null +++ b/seahub/admin_log/models.py @@ -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) diff --git a/seahub/admin_log/signals.py b/seahub/admin_log/signals.py new file mode 100644 index 0000000000..2751f0249a --- /dev/null +++ b/seahub/admin_log/signals.py @@ -0,0 +1,4 @@ +# Copyright (c) 2012-2017 Seafile Ltd. +import django.dispatch + +admin_operation = django.dispatch.Signal(providing_args=["admin_name", "operation", "detail"]) diff --git a/seahub/admin_log/views.py b/seahub/admin_log/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/seahub/admin_log/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/seahub/api2/endpoints/admin/groups.py b/seahub/api2/endpoints/admin/groups.py index 2e7a2c66f7..f5c2afafd9 100644 --- a/seahub/api2/endpoints/admin/groups.py +++ b/seahub/api2/endpoints/admin/groups.py @@ -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 """ + 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: - group_id = int(group_id) 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}) diff --git a/seahub/api2/endpoints/admin/libraries.py b/seahub/api2/endpoints/admin/libraries.py index 031d7849f2..1258c6b182 100644 --- a/seahub/api2/endpoints/admin/libraries.py +++ b/seahub/api2/endpoints/admin/libraries.py @@ -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) diff --git a/seahub/api2/endpoints/admin/logs.py b/seahub/api2/endpoints/admin/logs.py new file mode 100644 index 0000000000..54d4671771 --- /dev/null +++ b/seahub/api2/endpoints/admin/logs.py @@ -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 diff --git a/seahub/api2/endpoints/admin/utils.py b/seahub/api2/endpoints/admin/utils.py index a6c0f21b38..7fad56356e 100644 --- a/seahub/api2/endpoints/admin/utils.py +++ b/seahub/api2/endpoints/admin/utils.py @@ -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 diff --git a/seahub/api2/permissions.py b/seahub/api2/permissions.py index e307ed0d4b..a3643918e3 100644 --- a/seahub/api2/permissions.py +++ b/seahub/api2/permissions.py @@ -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() diff --git a/seahub/settings.py b/seahub/settings.py index 0b40098109..71edd5bc84 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -214,6 +214,7 @@ INSTALLED_APPS = ( 'seahub.help', 'seahub.thumbnail', 'seahub.password_session', + 'seahub.admin_log', ) # Enabled or disable constance(web settings). diff --git a/seahub/templates/js/sysadmin-templates.html b/seahub/templates/js/sysadmin-templates.html index 7570013fd6..d727dd2581 100644 --- a/seahub/templates/js/sysadmin-templates.html +++ b/seahub/templates/js/sysadmin-templates.html @@ -67,6 +67,10 @@ {% trans "Terms and Conditions" %} {% endif %} +
  • + {% trans "Admin Logs" %} +
  • + <% if (cur_tab == 'libraries') { %> <% if (option == 'all') { %> @@ -839,3 +843,75 @@ <% } %> + + + + diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index 46d59a8166..54ef3b1eeb 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -73,6 +73,9 @@ {% trans "Terms and Conditions" %} {% endif %} +
  • + {% trans "Admin Logs" %} +
  • {% endblock %} diff --git a/seahub/urls.py b/seahub/urls.py index 4599d94472..cbacceb12a 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -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[-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'), diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 7acc0f31d8..1f6d39f118 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -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.')) diff --git a/static/scripts/common.js b/static/scripts/common.js index c1fadf90d1..e89d4cca50 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -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/'; } }, diff --git a/static/scripts/sysadmin-app/collection/admin-logs.js b/static/scripts/sysadmin-app/collection/admin-logs.js new file mode 100644 index 0000000000..02c8837c58 --- /dev/null +++ b/static/scripts/sysadmin-app/collection/admin-logs.js @@ -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; +}); diff --git a/static/scripts/sysadmin-app/models/admin-log.js b/static/scripts/sysadmin-app/models/admin-log.js new file mode 100644 index 0000000000..b1e7b1d105 --- /dev/null +++ b/static/scripts/sysadmin-app/models/admin-log.js @@ -0,0 +1,11 @@ +define([ + 'underscore', + 'backbone', + 'common', +], function(_, Backbone, Common) { + 'use strict'; + + var AdminLogModel = Backbone.Model.extend({}); + + return AdminLogModel; +}); diff --git a/static/scripts/sysadmin-app/router.js b/static/scripts/sysadmin-app/router.js index 46ce2b1870..9fdb0260d7 100644 --- a/static/scripts/sysadmin-app/router.js +++ b/static/scripts/sysadmin-app/router.js @@ -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}); } }); diff --git a/static/scripts/sysadmin-app/views/admin-log.js b/static/scripts/sysadmin-app/views/admin-log.js new file mode 100644 index 0000000000..adceb41d22 --- /dev/null +++ b/static/scripts/sysadmin-app/views/admin-log.js @@ -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; +}); diff --git a/static/scripts/sysadmin-app/views/admin-logs.js b/static/scripts/sysadmin-app/views/admin-logs.js new file mode 100644 index 0000000000..330bc79e96 --- /dev/null +++ b/static/scripts/sysadmin-app/views/admin-logs.js @@ -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; +});