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 %}
+
+  |
+
+ {{ sfile.formatted_path }}
+ |
+ {{ sfile.repo.name }} |
+ {{ sfile.last_modified|translate_commit_time }} |
+
+ {% endfor %}
+
+{% 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)