diff --git a/base/models.py b/base/models.py index 8f6e19a108..f9eaf3bd51 100644 --- a/base/models.py +++ b/base/models.py @@ -149,3 +149,15 @@ def innerpubmsg_reply_added_cb(sender, instance, **kwargs): detail=msg_id) n.save() +class UserStarredFiles(models.Model): + """Starred files are marked by users to get quick access to it on user + home page. + + """ + + email = models.EmailField() + org_id = models.IntegerField() + repo_id = models.CharField(max_length=36) + + path = models.TextField() + is_dir = models.BooleanField() diff --git a/media/css/seahub.css b/media/css/seahub.css index 27427d0767..063b8efdaf 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -666,7 +666,8 @@ textarea:-moz-placeholder { #repo-create-form { padding:0 20px; } -.empty-repo-tips { +.empty-repo-tips, +.no-starred-file-tips { width: 60%; margin: 25px auto; } @@ -1349,6 +1350,10 @@ textarea:-moz-placeholder { #file-edit-cancel { color:#900; } +#star { + padding-left:18px; + background:#efefef url('../img/star.png') no-repeat scroll 3px 48%; +} /* shareadmin */ .view-link-alert p { display: inline-block; diff --git a/media/img/star.png b/media/img/star.png new file mode 100644 index 0000000000..be18a04603 Binary files /dev/null and b/media/img/star.png differ diff --git a/organizations/templates/organizations/personal.html b/organizations/templates/organizations/personal.html index 81ccd74b2b..6ce6d68b64 100644 --- a/organizations/templates/organizations/personal.html +++ b/organizations/templates/organizations/personal.html @@ -32,7 +32,7 @@ {% endblock %} {% block right_panel %} - + {% include "snippets/my_owned_repos.html" %} {% if events %} diff --git a/organizations/views.py b/organizations/views.py index 55459f3ae3..98318fa1f6 100644 --- a/organizations/views.py +++ b/organizations/views.py @@ -37,7 +37,7 @@ from seahub.forms import RepoCreateForm, SharedRepoCreateForm import seahub.settings as seahub_settings from seahub.utils import render_error, render_permission_error, gen_token, \ validate_group_name, string2list, calculate_repo_last_modify, MAX_INT, \ - EVENTS_ENABLED, get_org_user_events + EVENTS_ENABLED, get_org_user_events, get_starred_files from seahub.views import myhome from seahub.signals import repo_created @@ -141,6 +141,7 @@ def org_personal(request, url_prefix): events = None quota_usage = seafserv_threaded_rpc.get_org_user_quota_usage(org.org_id, user) + starred_files = get_starred_files(user, org_id=org.org_id) return render_to_response('organizations/personal.html', { 'owned_repos': owned_repos, @@ -154,6 +155,7 @@ def org_personal(request, url_prefix): 'nickname': nickname, 'events': events, 'quota_usage': quota_usage, + 'starred_files': starred_files, }, context_instance=RequestContext(request)) @login_required diff --git a/templates/myhome.html b/templates/myhome.html index 3be824281e..0451cdde2b 100644 --- a/templates/myhome.html +++ b/templates/myhome.html @@ -19,30 +19,30 @@ {% if grpmsg_list or grpmsg_reply_list or orgmsg_list %}
-

{% trans "Reminders..." %}

- +

{% trans "Reminding..." %}

+
{% endif %}
-

{% trans "Storage Used" %}

-

{{ quota_usage|filesizeformat }} {% if quota > 0 %}/ {{ quota|filesizeformat }} {% endif %}

+

{% trans "Storage Used" %}

+

{{ quota_usage|filesizeformat }} {% if quota > 0 %}/ {{ quota|filesizeformat }} {% endif %}

@@ -52,7 +52,7 @@ {% endblock %} {% block right_panel %} - + {% include "snippets/my_owned_repos.html" %} {% if events %} diff --git a/templates/repo_view_file.html b/templates/repo_view_file.html index 5ff6a94bcf..f56fafbbe9 100644 --- a/templates/repo_view_file.html +++ b/templates/repo_view_file.html @@ -25,14 +25,12 @@ {% endif %} - {% if page_from == 'recycle' %} -
-

- {{repo.props.name}} 的文件回收站 -

- -
- {% endif %} + {% if page_from == 'recycle' %} +
+

{{repo.props.name}} 的文件回收站

+ +
+ {% endif %} {% endif %}

@@ -65,7 +63,6 @@ {{ name }} {% endif %} {% endfor %} - {% endif %}

@@ -75,6 +72,11 @@ + {% if is_starred %} + + {% else %} + + {% endif %}
{% endif %} @@ -465,6 +467,48 @@ $('#open-local').click(function () { } }, 2000); }); +//star +$('#star').click(function() { + var star_btn = $(this); + var state = star_btn.attr('data'); + $.ajax({ + url: '{{ SITE_ROOT }}repo/star_file/{{ repo.id }}/', + type: 'POST', + cache: false, + contentType: 'application/json; charset=utf-8', + beforeSend: prepareCSRFToken, + dataType: 'json', + data: { + path: '{{ path }}', + state: state, + org_id: {% if org %} {{ org.org_id }} {% else %} -1 {% endif %} + }, + success:function(data) { + if (data['success']) { + if (state == 'starred') { + feedback('星标取消成功', 'success'); + star_btn.attr('data', 'unstarred').text('添加星标'); + } else { + feedback('星标添加成功', 'success'); + star_btn.attr('data', 'starred').text('取消星标'); + } + } else { + feedback('操作失败:' + data['err_msg'], 'error'); + } + }, + error:function(jqXHR, textStatus, errorThrown) { + feedback(textStatus + ',操作失败', 'error'); + } + }); +}) +.hover( + function() { + $(this).css('background-color', '#fff'); + }, + function() { + $(this).css('background-color', '#efefef'); + } +); //'not view_history' ends here {% endif %} diff --git a/templates/snippets/my_owned_repos.html b/templates/snippets/my_owned_repos.html index a12cd9ca52..23b8b29b95 100644 --- a/templates/snippets/my_owned_repos.html +++ b/templates/snippets/my_owned_repos.html @@ -1,10 +1,12 @@ {% load seahub_tags i18n %} +{% load url from future %}

{% trans "Libraries" %}

@@ -82,4 +84,28 @@

您的朋友可以将他的资料库共享给您,这些资料库会显示在这里。

{% endif %}
+
+{% if starred_files %} + + + + + + + + {% for sfile in starred_files %} + + + + + + + {% endfor %} +
文件名所属资料库更新时间
文件 + {{ sfile.formatted_path }} + {{ sfile.repo.name }}{{ sfile.last_modified|translate_commit_time }}
+{% else %} +

您可以把重要的文件加上星标,这样它们就能显示在这里

+{% endif %} +
diff --git a/urls.py b/urls.py index edf1e13748..3a5e026a3b 100644 --- a/urls.py +++ b/urls.py @@ -53,6 +53,7 @@ urlpatterns = patterns('', url(r'^repo/revert_dir/(?P[^/]+)/$', repo_revert_dir, name='repo_revert_dir'), url(r'^repo/upload_file/(?P[^/]+)/$', repo_upload_file, name='repo_upload_file'), url(r'^repo/update_file/(?P[^/]+)/$', repo_update_file, name='repo_update_file'), + url(r'^repo/star_file/(?P[^/]+)/$', repo_star_file, name='repo_star_file'), (r'^repo/upload_error/(?P[^/]+)/$', upload_file_error), (r'^repo/update_error/(?P[^/]+)/$', update_file_error), url(r'^repo/file_revisions/(?P[^/]+)/$', file_revisions, name='file_revisions'), diff --git a/utils.py b/utils.py index b2878c4447..c0221e2f62 100644 --- a/utils.py +++ b/utils.py @@ -4,12 +4,14 @@ import os import re import random import stat +import urllib2 from django.shortcuts import render_to_response from django.template import RequestContext from django.utils.hashcompat import sha_constructor -from base.models import FileContributors +from base.models import FileContributors, UserStarredFiles +from django.utils.hashcompat import md5_constructor from pysearpc import SearpcError @@ -468,3 +470,126 @@ else: pass def get_org_user_events(): pass + +class StarredFile(object): + def format_path(self): + if self.path == "/": + return self.path + + # strip leading slash + path = self.path[1:] + if path[-1:] == '/': + path = path[:-1] + return path.replace('/', ' / ') + + def __init__(self, org_id, repo, path, is_dir, last_modified): + # always 0 for non-org repo + self.org_id = org_id + self.repo = repo + self.path = path + self.formatted_path = self.format_path() + self.is_dir = is_dir + # always 0 for dir + self.last_modified = last_modified + +# org_id > 0: get starred files in org repos +# org_id < 0: get starred files in personal repos +def get_starred_files(email, org_id=-1): + """Return a list of starred files for some user, sorted descending by the + last modified time. + + """ + starred_files = UserStarredFiles.objects.filter(email=email, org_id=org_id) + + ret = [] + for sfile in starred_files: + # repo still exists? + try: + repo = get_repo(sfile.repo_id) + except SearpcError: + continue + + if not repo: + sfile.delete() + continue + + # file still exists? + file_id = '' + if sfile.path != "/": + try: + file_id = seafserv_threaded_rpc.get_file_by_path(sfile.repo_id, sfile.path) + except SearpcError: + continue + + if not file_id: + sfile.delete() + continue + + last_modified = 0 + if not sfile.is_dir: + # last modified + path_hash = md5_constructor(urllib2.quote(sfile.path.encode('utf-8'))).hexdigest()[:12] + last_modified = get_file_contributors(sfile.repo_id, sfile.path, path_hash, file_id)[1] + + f = StarredFile(sfile.org_id, repo, sfile.path, sfile.is_dir, last_modified) + + ret.append(f) + + # First sort by repo + # Within the same repo: + # dir > file + # dirs are sorted by name ascending + # files are sorted by last_modified descending + def sort_func(fa, fb): + # Different repo? + if fa.repo.id != fb.repo.id: + ret = cmp(fa.repo.name, fb.repo.name) + if ret != 0: + return ret + else: + # two different repo has the same name, compare the id + return cmp(fa.repo.id, fb.repo.id) + + # OK, same repo + if fa.is_dir and fb.is_dir: + return cmp(fa.path, fb.path) + elif fa.is_dir and not fb.is_dir: + return -1 + elif fb.is_dir and not fa.is_dir: + return 1 + else: + return cmp(fb.last_modified, fa.last_modified) + + ret.sort(sort_func) + return ret + +def star_file(email, repo_id, path, is_dir, org_id=-1): + if is_file_starred(email, repo_id, path, org_id): + return + + f = UserStarredFiles(email=email, + org_id=org_id, + repo_id=repo_id, + path=path, + is_dir=is_dir) + f.save() + +def unstar_file(email, repo_id, path): + try: + f = UserStarredFiles.objects.get(email=email, + repo_id=repo_id, + path=path) + except UserStarredFiles.DoesNotExist: + pass + else: + f.delete() + +def is_file_starred(email, repo_id, path, org_id=-1): + try: + f = UserStarredFiles.objects.get(email=email, + repo_id=repo_id, + path=path, + org_id=org_id) + return True + except UserStarredFiles.DoesNotExist: + return False diff --git a/views.py b/views.py index db55b9bf3f..598f5bfe7f 100644 --- a/views.py +++ b/views.py @@ -68,7 +68,8 @@ from utils import render_permission_error, render_error, list_to_string, \ get_file_revision_id_size, get_ccnet_server_addr_port, \ gen_file_get_url, string2list, MAX_INT, \ gen_file_upload_url, check_and_get_org_by_repo, \ - get_file_contributors, EVENTS_ENABLED, get_user_events + get_file_contributors, EVENTS_ENABLED, get_user_events, \ + get_starred_files, star_file, unstar_file, is_file_starred try: from settings import DOCUMENT_CONVERTOR_ROOT if DOCUMENT_CONVERTOR_ROOT[-1:] != '/': @@ -282,6 +283,14 @@ class RepoView(CtxSwitchRequiredMixin, RepoMixin, TemplateResponseMixin, is_group_user(x.id, self.user.username)] return groups + def is_starred_dir(self): + org_id = -1 + if self.request.user.org: + org_id = self.request.user.org['org_id'] + args = (self.request.user.username, self.repo.id, self.path.encode('utf-8'), org_id) + print args + return is_file_starred(*args) + def get_context_data(self, **kwargs): kwargs['repo'] = self.repo # kwargs['can_access'] = self.can_access @@ -296,6 +305,7 @@ class RepoView(CtxSwitchRequiredMixin, RepoMixin, TemplateResponseMixin, kwargs['accessible_repos'] = self.get_accessible_repos() kwargs['applet_root'] = self.applet_root kwargs['groups'] = self.get_repo_shared_groups() + kwargs['is_starred'] = self.is_starred_dir() if len(kwargs['groups']) > 1: ctx = {} ctx['groups'] = kwargs['groups'] @@ -919,6 +929,8 @@ def myhome(request): else: events = None + starred_files = get_starred_files(request.user.username) + return render_to_response('myhome.html', { "nickname": nickname, "owned_repos": owned_repos, @@ -935,6 +947,7 @@ def myhome(request): "create_shared_repo": False, "allow_public_share": allow_public_share, "events": events, + "starred_files": starred_files, }, context_instance=RequestContext(request)) @login_required @@ -1308,6 +1321,13 @@ def repo_view_file(request, repo_id): else: repogrp_str = '' + is_starred = False + if page_from != 'recycle': + org_id = -1 + if request.user.org: + org_id = request.user.org['org_id'] + is_starred = is_file_starred(request.user.username, repo.id, path.encode('utf-8'), org_id) + return render_to_response('repo_view_file.html', { 'repo': repo, 'obj_id': obj_id, @@ -1342,6 +1362,7 @@ def repo_view_file(request, repo_id): 'read_only': read_only, 'page_from': page_from, 'repo_group_str': repogrp_str, + 'is_starred': is_starred, }, context_instance=RequestContext(request)) def file_comment(request): @@ -2752,3 +2773,22 @@ def i18n(request): return res +@login_required +def repo_star_file(request, repo_id): + path = request.POST.get('path') + state = request.POST.get('state'); + + content_type = 'application/json; charset=utf-8' + + if not (path and state): + return HttpResponse(json.dumps({'success':False, 'err_msg':u'参数错误'}), + content_type=content_type) + + org_id = int(request.POST.get('org_id')) + path = urllib2.unquote(path.encode('utf-8')) + is_dir = False + if state == 'unstarred': + star_file(request.user.username, repo_id, path, is_dir, org_id=org_id) + else: + unstar_file(request.user.username, repo_id, path) + return HttpResponse(json.dumps({'success':True}), content_type=content_type)