From 643d036942f58172e694565a16227b4f6dd22b3c Mon Sep 17 00:00:00 2001 From: lian Date: Fri, 27 Nov 2015 10:55:56 +0800 Subject: [PATCH] new file access log page --- media/css/seahub.css | 9 ++ seahub/templates/file_access.html | 91 ++++++++++++++++++ seahub/templates/js/templates.html | 1 + seahub/urls.py | 3 +- seahub/utils/__init__.py | 28 ++++++ seahub/views/file.py | 82 +++++++++++++++- tests/seahub/views/test_file.py | 144 +++++++++++++++++++++++++++++ 7 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 seahub/templates/file_access.html create mode 100644 tests/seahub/views/test_file.py diff --git a/media/css/seahub.css b/media/css/seahub.css index b0b4608ca7..19c735d5f0 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -1198,6 +1198,7 @@ textarea:-moz-placeholder {/* for FF */ .tabnav, .repo-file-list-topbar, .commit-list-topbar, +.file-audit-list-topbar, #dir-view .repo-op, .wiki-top { padding:9px 10px; @@ -1219,6 +1220,7 @@ textarea:-moz-placeholder {/* for FF */ position:relative; } .commit-list-topbar, +.file-audit-list-topbar, .repo-file-list-topbar { margin-bottom:0; } @@ -1822,6 +1824,7 @@ textarea:-moz-placeholder {/* for FF */ margin: 2px 0 5px; } .repo-file-list-outer-container, +.file-audit-list-outer-container, .commit-list-outer-container { padding:3px; background:#eee; @@ -1829,6 +1832,7 @@ textarea:-moz-placeholder {/* for FF */ margin:10px 0 25px; } .repo-file-list-inner-container, +.file-audit-list-inner-container, .commit-list-inner-container { min-height:250px; background:#fff; @@ -1838,10 +1842,12 @@ textarea:-moz-placeholder {/* for FF */ .repo-file-list-not-show { padding-left:10px; } +.file-audit-list-inner-container .file-audit-list-topbar, .commit-list-inner-container .commit-list-topbar, .repo-file-list-inner-container .repo-file-list-topbar { padding:8px 10px; } +.file-audit-list-topbar .path, .commit-list-topbar .path, .repo-file-list-topbar .path { font-size:14px; @@ -2181,12 +2187,15 @@ textarea:-moz-placeholder {/* for FF */ #back { margin-top:3px; } +.file-audit-list, .commit-list { margin:0 0 20px; } +.file-audit-list .user, .commit-list .time { padding-left:10px; } +.file-audit-list .avatar, .commit-list .avatar { border-radius:2px; } diff --git a/seahub/templates/file_access.html b/seahub/templates/file_access.html new file mode 100644 index 0000000000..fadf2dc29c --- /dev/null +++ b/seahub/templates/file_access.html @@ -0,0 +1,91 @@ +{% extends base_template %} +{% load seahub_tags avatar_tags i18n %} +{% load url from future %} + +{% block sub_title %}{% trans "Access Log" %} - {% endblock %} + +{% block main_panel %} +

{{ filename }} {% trans "Access Log" %}

+
+
+
+

+ {% trans 'Current Path:' %} + {% for name, link in zipped %} + {% if not forloop.last %} + {{ name }} / + {% else %} + {{ name }} + {% endif %} + {% endfor %} +

+
+ + {% if events %} + + + + + + + + + {% for e in events %} + + + + + + + {% endfor %} +
{% trans "User" %}{% trans "Type" %}{% trans "IP / Device" %}{% trans "Date" %}
+ {% avatar e.user 16 %} + {% if e.user %} + {{ e.user }} + {% else %} + {% trans "Anonymous User" %} + {% endif %} + {{ e.event_type }} + {% if e.show_device %} + {{ e.ip }} / {{ e.show_device }} + {% else %} + {{ e.ip }} + {% endif %} + {{ e.time|date:"Y-m-d G:i:s" }}
+ +
+ {% if current_page != 1 %} + {% trans "Previous" %} + {% endif %} + {% if page_next %} + {% trans "Next" %} + {% endif %} + {% if current_page != 1 or page_next %} + | + {% endif %} + {% trans "Per page: " %} + {% if per_page == 25 %} + 25 + {% else %} + 25 + {% endif %} + {% if per_page == 50 %} + 50 + {% else %} + 50 + {% endif %} + {% if per_page == 100 %} + 100 + {% else %} + 100 + {% endif %} +
+ {% else %} +
+

{% trans "No file access infomation" %}

+
+ {% endif %} + +
+
+{% endblock %} diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 4941359463..221525611a 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -296,6 +296,7 @@
  • {% trans "Copy" %}
  • {% trans "History" %}
  • <% if (is_pro) { %> +
  • {% trans "Access Log" %}
  • <% if (dirent.is_locked) { %> <% if (dirent.locked_by_me) { %>
  • {% trans "Unlock" %}
  • diff --git a/seahub/urls.py b/seahub/urls.py index 6a3929da6b..73a2659e62 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -7,7 +7,7 @@ from seahub.views import * from seahub.views.file import view_repo_file, view_history_file, view_trash_file,\ view_snapshot_file, file_edit, view_shared_file, view_file_via_shared_dir,\ text_diff, view_priv_shared_file, view_raw_file, view_raw_shared_file, \ - download_file, view_lib_file + download_file, view_lib_file, file_access from seahub.views.repo import repo, repo_history_view, view_shared_dir, \ view_shared_upload_link from notifications.views import notification_list @@ -70,6 +70,7 @@ urlpatterns = patterns( (r'^repo/upload_error/(?P[-0-9a-f]{36})/$', upload_file_error), (r'^repo/update_error/(?P[-0-9a-f]{36})/$', update_file_error), url(r'^repo/file_revisions/(?P[-0-9a-f]{36})/$', file_revisions, name='file_revisions'), + url(r'^repo/file-access/(?P[-0-9a-f]{36})/$', file_access, name='file_access'), url(r'^repo/text_diff/(?P[-0-9a-f]{36})/$', text_diff, name='text_diff'), url(r'^repo/(?P[-0-9a-f]{36})/$', repo, name='repo'), url(r'^repo/history/(?P[-0-9a-f]{36})/$', repo_history, name='repo_history'), diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index b60e612c27..697fd06d03 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -641,6 +641,30 @@ if EVENTS_CONFIG_FILE: def get_org_user_events(org_id, username, start, count): return _get_events(username, start, count, org_id=org_id) + def generate_file_audit_event_type(e): + return { + 'file-download-web': (_('web'), ''), + 'file-download-share-link': (_('share-link'),''), + 'file-download-api': (_('API'), e.device), + 'repo-download-sync': (_('download-sync'), e.device), + 'repo-upload-sync': (_('upload-sync'), e.device), + }[e.etype] + + def get_file_audit_events_by_path(email, org_id, repo_id, file_path, start, limit): + """Return file audit events list by file path. (If no file audit, return 'None') + + For example: + ``get_file_audit_events_by_path(email, org_id, repo_id, file_path, 0, 10)`` returns the first 10 + events. + ``get_file_audit_events_by_path(email, org_id, repo_id, file_path, 5, 10)`` returns the 6th through + 15th events. + """ + with _get_seafevents_session() as session: + events = seafevents.get_file_audit_events_by_path(session, + email, org_id, repo_id, file_path, start, limit) + + return events if events else None + def get_file_audit_events(email, org_id, repo_id, start, limit): """Return file audit events list. (If no file audit, return 'None') @@ -701,6 +725,10 @@ else: pass def get_org_user_events(): pass + def generate_file_audit_event_type(): + pass + def get_file_audit_events_by_path(): + pass def get_file_audit_events(): pass def get_file_update_events(): diff --git a/seahub/views/file.py b/seahub/views/file.py index d0012ab766..dae4fd4baa 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -56,8 +56,10 @@ from seahub.utils import show_delete_days, render_error, is_org_context, \ render_permission_error, is_pro_version, \ is_textual_file, mkstemp, EMPTY_SHA1, HtmlDiff, \ check_filename_with_rename, gen_inner_file_get_url, normalize_file_path, \ - user_traffic_over_limit, do_md5 + user_traffic_over_limit, do_md5, get_file_audit_events_by_path, \ + generate_file_audit_event_type from seahub.utils.ip import get_remote_ip +from seahub.utils.timeutils import utc_to_local from seahub.utils.file_types import (IMAGE, PDF, DOCUMENT, SPREADSHEET, AUDIO, MARKDOWN, TEXT, OPENDOCUMENT, VIDEO) from seahub.utils.star import is_file_starred @@ -1568,3 +1570,81 @@ def view_priv_shared_file(request, token): 'accessible_repos': accessible_repos, 'save_to_link': save_to_link, }, context_instance=RequestContext(request)) + +@login_required +def file_access(request, repo_id): + """List file access log. + """ + + if not is_pro_version(): + raise Http404 + + referer = request.META.get('HTTP_REFERER', None) + next = settings.SITE_ROOT if referer is None else referer + + repo = get_repo(repo_id) + if not repo: + messages.error(request, _("Library does not exist")) + return HttpResponseRedirect(next) + + path = request.GET.get('p', None) + if not path: + messages.error(request, _("Argument missing")) + return HttpResponseRedirect(next) + + if not seafile_api.get_file_id_by_path(repo_id, path): + messages.error(request, _("File does not exist")) + return HttpResponseRedirect(next) + + # perm check + if check_folder_permission(request, repo_id, path) != 'rw': + messages.error(request, _("Permission denied")) + return HttpResponseRedirect(next) + + # Make sure page request is an int. If not, deliver first page. + try: + current_page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + current_page = 1 + per_page = 100 + + start = per_page * (current_page - 1) + limit = per_page + 1 + + if is_org_context(request): + org_id = request.user.org.org_id + events = get_file_audit_events_by_path(None, org_id, repo_id, path, start, limit) + else: + events = get_file_audit_events_by_path(None, 0, repo_id, path, start, limit) + + events = events if events else [] + if len(events) == per_page + 1: + page_next = True + else: + page_next = False + + events = events[:per_page] + + for ev in events: + ev.repo = get_repo(ev.repo_id) + ev.filename = os.path.basename(ev.file_path) + ev.time = utc_to_local(ev.timestamp) + ev.event_type, ev.show_device = generate_file_audit_event_type(ev) + + filename = os.path.basename(path) + zipped = gen_path_link(path, repo.name) + extra_href = "&p=%s" % urlquote(path) + return render_to_response('file_access.html', { + 'repo': repo, + 'path': path, + 'filename': filename, + 'zipped': zipped, + 'events': events, + 'extra_href': extra_href, + 'current_page': current_page, + 'prev_page': current_page-1, + 'next_page': current_page+1, + 'per_page': per_page, + 'page_next': page_next, + }, context_instance=RequestContext(request)) diff --git a/tests/seahub/views/test_file.py b/tests/seahub/views/test_file.py new file mode 100644 index 0000000000..4471f1e899 --- /dev/null +++ b/tests/seahub/views/test_file.py @@ -0,0 +1,144 @@ +import datetime + +from mock import patch + +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase + + +class FileAccessLogTest(BaseTestCase): + + def setUp(self): + self.login_as(self.user) + self.file_path = self.file + self.repo_id = self.repo.id + + def tearDown(self): + self.remove_repo() + + def generate_file_audit_event_type(self, e): + return { + 'file-download-web': ('web', ''), + 'file-download-share-link': ('share-link',''), + 'file-download-api': ('API', e.device), + 'repo-download-sync': ('download-sync', e.device), + 'repo-upload-sync': ('upload-sync', e.device), + }[e.etype] + + @patch('seahub.views.file.is_pro_version') + def test_can_not_render_if_not_pro(self, mock_is_pro_version): + mock_is_pro_version.return_value = False + + url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path + resp = self.client.get(url) + self.assertEqual(404, resp.status_code) + + @patch('seahub.views.file.generate_file_audit_event_type') + @patch('seahub.views.file.get_file_audit_events_by_path') + @patch('seahub.views.file.is_pro_version') + def test_can_show_web_type(self, mock_is_pro_version, + mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type): + + etype = 'file-download-web' + event = Event(self.user.email, self.repo_id, self.file_path, etype) + + mock_is_pro_version.return_value = True + mock_get_file_audit_events_by_path.return_value = [event] + mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type + + url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, 'file_access.html') + self.assertContains(resp, 'web') + + @patch('seahub.views.file.generate_file_audit_event_type') + @patch('seahub.views.file.get_file_audit_events_by_path') + @patch('seahub.views.file.is_pro_version') + def test_can_show_share_link_type(self, mock_is_pro_version, + mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type): + + etype = 'file-download-share-link' + event = Event(self.user.email, self.repo_id, self.file_path, etype) + + mock_is_pro_version.return_value = True + mock_get_file_audit_events_by_path.return_value = [event] + mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type + + url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, 'file_access.html') + self.assertContains(resp, 'share-link') + + @patch('seahub.views.file.generate_file_audit_event_type') + @patch('seahub.views.file.get_file_audit_events_by_path') + @patch('seahub.views.file.is_pro_version') + def test_can_show_api_type(self, mock_is_pro_version, + mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type): + + etype = 'file-download-api' + event = Event(self.user.email, self.repo_id, self.file_path, etype) + + mock_is_pro_version.return_value = True + mock_get_file_audit_events_by_path.return_value = [event] + mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type + + url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, 'file_access.html') + self.assertContains(resp, 'API') + + @patch('seahub.views.file.generate_file_audit_event_type') + @patch('seahub.views.file.get_file_audit_events_by_path') + @patch('seahub.views.file.is_pro_version') + def test_can_show_download_sync_type(self, mock_is_pro_version, + mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type): + + etype = 'repo-download-sync' + event = Event(self.user.email, self.repo_id, self.file_path, etype) + + mock_is_pro_version.return_value = True + mock_get_file_audit_events_by_path.return_value = [event] + mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type + + url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, 'file_access.html') + self.assertContains(resp, 'download-sync') + + @patch('seahub.views.file.generate_file_audit_event_type') + @patch('seahub.views.file.get_file_audit_events_by_path') + @patch('seahub.views.file.is_pro_version') + def test_can_show_upload_sync_type(self, mock_is_pro_version, + mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type): + + etype = 'repo-upload-sync' + event = Event(self.user.email, self.repo_id, self.file_path, etype) + + mock_is_pro_version.return_value = True + mock_get_file_audit_events_by_path.return_value = [event] + mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type + + url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, 'file_access.html') + self.assertContains(resp, 'upload-sync') + + +class Event(object): + + def __init__(self, user, repo_id, file_path, etype): + + self.device = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36' + self.ip = '192.168.1.124' + self.org_id = -1 + self.timestamp = datetime.datetime.now() + self.user = user + self.repo_id = repo_id + self.file_path = file_path + self.etype = etype