1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 14:50:29 +00:00

Merge pull request #1655 from haiwen/deleted-repos

Deleted repos
This commit is contained in:
Daniel Pan
2017-07-01 12:04:11 +08:00
committed by GitHub
12 changed files with 448 additions and 14 deletions

View File

@@ -0,0 +1,70 @@
import logging
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.template.defaultfilters import filesizeformat
from seaserv import seafile_api
from pysearpc import SearpcError
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.base import APIView
from seahub.api2.utils import api_error
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
logger = logging.getLogger(__name__)
class DeletedRepos(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request):
"""
get the deleted-repos of owner
"""
trashs_json = []
email = request.user.username
trash_repos = seafile_api.get_trash_repos_by_owner(email)
for r in trash_repos:
trash = {
"repo_id": r.repo_id,
"owner_email": email,
"owner_name": email2nickname(email),
"owner_contact_email": email2contact_email(email),
"repo_name": r.repo_name,
"org_id": r.org_id,
"head_commit_id": r.head_id,
"encrypted": r.encrypted,
"del_time": timestamp_to_isoformat_timestr(r.del_time),
"size": r.size,
}
trashs_json.append(trash)
return Response(trashs_json)
def post(self, request):
"""
restore deleted-repo
return:
return True if success, otherwise api_error
"""
post_data = request.POST
repo_id = post_data.get('repo_id', '')
if not repo_id:
error_msg = "repo_id invalid"
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
try:
seafile_api.restore_repo_from_trash(repo_id)
except SearpcError as e:
logger.error(e)
error = "Internal Server Error"
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({"success": True})

View File

@@ -154,6 +154,30 @@
</div> </div>
</script> </script>
<script type="text/template" id="deleted-repo-tmpl">
<td>
<img src="<%= icon_url %>" title="<%= icon_title %>" alt="<%= icon_title %>" width="24" />
</td>
<td><%- repo_name %></td>
<td><span title="<%= time %>"><%= time_from_now %></span></td>
<td>
<a href="#" class="sf2-icon-reply sf2-x restore op-icon vh" title="{% trans "Restore" %}" aria-label="{% trans "Restore" %}"></a>
</td>
</script>
<script type="text/template" id="deleted-repo-mobile-tmpl">
<td>
<img src="<%= icon_url %>" title="<%= icon_title %>" alt="<%= icon_title %>" width="24" />
</td>
<td>
<span class="repo-name-span">
<%- repo_name %><br />
<span class="repo-meta-info" title="<%= time %>"><%= time_from_now %></span>
</span>
</td>
<td>
<a href="#" class="sf2-icon-reply sf2-x restore op-icon" title="{% trans "Restore" %}" aria-label="{% trans "Restore" %}"></a>
</td>
</script>
<script type="text/template" id="group-repo-tmpl"> <script type="text/template" id="group-repo-tmpl">
<td> <td>
@@ -1260,13 +1284,13 @@
<script type="text/template" id="my-repos-hd-tmpl"> <script type="text/template" id="my-repos-hd-tmpl">
<tr> <tr>
<th width="4%"><span class="sr-only">{% trans "Library Type" %}</span><!--icon--></th> <th width="4%"><span class="sr-only">{% trans "Library Type" %}</span><!--icon--></th>
<th width="42%"><a class="table-sort-op by-name" href="#">{% trans "Name" %} <span class="sort-icon icon-caret-down hide"></span></a></th> <th width="42%"><a class="table-sort-op by-name" href="#">{% trans "Name" %} <span class="sort-icon icon-caret-down hide"></span></a></th>
<th width="14%"><span class="sr-only">{% trans "Actions" %}</span></th> <th width="14%"><span class="sr-only">{% trans "Actions" %}</span></th>
<th width="20%">{% trans "Size" %}</th> <th width="20%">{% trans "Size" %}</th>
<th width="20%"><a class="table-sort-op by-time" href="#">{% trans "Last Update" %} <span class="sort-icon icon-caret-up"></span></a></th> <th width="20%"><a class="table-sort-op by-time" href="#">{% trans "Last Update" %} <span class="sort-icon icon-caret-up"></span></a></th>
</tr> </tr>
</script> </script>
<script type="text/template" id="my-repos-hd-mobile-tmpl"> <script type="text/template" id="my-repos-hd-mobile-tmpl">
<tr> <tr>
@@ -1278,6 +1302,23 @@
<th width="4%"><span class="sr-only">{% trans "Actions" %}</span></th> <th width="4%"><span class="sr-only">{% trans "Actions" %}</span></th>
</tr> </tr>
</script> </script>
<script type="text/template" id="my-deleted-repos-hd-tmpl">
<tr>
<th width="4%"><span class="sr-only">{% trans "Library Type" %}</span><!--icon--></th>
<th width="52%">{% trans "Name" %}</th>
<th width="30%">{% trans "Deleted Time" %}</th>
<th width="14%"><span class="sr-only">{% trans "Actions" %}</span></th>
</tr>
</script>
<script type="text/template" id="my-deleted-repos-hd-mobile-tmpl">
<tr class="vh">
<th width="4%"><span class="sr-only">{% trans "Library Type" %}</span><!--icon--></th>
<th width="82%">{% trans "Name" %}{% trans "Deleted Time" %}</th>
<th width="14%"><span class="sr-only">{% trans "Actions" %}</span></th>
</tr>
</script>
<script type="text/template" id="shared-repos-hd-tmpl"> <script type="text/template" id="shared-repos-hd-tmpl">
<tr> <tr>
<th width="4%"><span class="sr-only">{% trans "Library Type" %}</span><!--icon--></th> <th width="4%"><span class="sr-only">{% trans "Library Type" %}</span><!--icon--></th>

View File

@@ -9,9 +9,18 @@
{% block extra_script %} {% block extra_script %}
<script type="text/template" id="my-own-repos-tmpl"> <script type="text/template" id="my-own-repos-tmpl">
<div class="hd ovhd"> <div class="hd">
<h3 class="fleft">{% trans "My Libraries" %}</h3> <h3 class="fleft">{% trans "My Libraries" %}</h3>
<button class="repo-create btn-white fright"><span aria-hidden="true" class="icon-plus-square add vam"></span><span class="vam">{% trans "New Library" %}</span></button> <div class="fright">
<button class="repo-create btn-white"><span aria-hidden="true" class="icon-plus-square add vam"></span><span class="vam">{% trans "New Library" %}</span></button>
<div id="my-libs-more-op" class="sf-dropdown sf-dropdown-inline">
<button class="sf-dropdown-toggle btn-white more-op-btn-white">{% trans "More" %}</button>
<ul class="sf-dropdown-menu hide">
<li><a href="#my-libs/deleted/">{% trans "Deleted Libraries" %}</a></li>
</ul>
</div>
</div>
</div> </div>
<table class="my-own-repos-table hide"> <table class="my-own-repos-table hide">
<thead></thead> <thead></thead>
@@ -25,6 +34,25 @@
<p class="error error-tip hide"></p> <p class="error error-tip hide"></p>
</script> </script>
<script type="text/template" id="my-deleted-repos-tmpl">
<p class="path-bar">
<a href="#my-libs/" class="normal">{% trans "My Libraries" %}</a>
<span class="path-split">/</span>
<span>{% trans "Deleted" %}</span>
</p>
<p class="tip">{% trans "Tip: libraries deleted 30 days ago will be cleaned automatically."%}</p>
<table class="my-deleted-repos-table hide">
<thead></thead>
<tbody></tbody>
</table>
<div class="empty-tips hide">
<h2 class="alc">{% trans "No deleted libraries." %}</h2>
</div>
<span class="loading-icon loading-tip"></span>
<p class="error error-tip hide"></p>
</script>
<script type="text/template" id="repos-shared-to-me-tmpl"> <script type="text/template" id="repos-shared-to-me-tmpl">
<h3 class="hd">{% trans "Shared with me" %}</h3> <h3 class="hd">{% trans "Shared with me" %}</h3>
<table class="hide"> <table class="hide">

View File

@@ -30,6 +30,7 @@ from seahub.api2.endpoints.repos import RepoView
from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.file import FileView
from seahub.api2.endpoints.dir import DirView from seahub.api2.endpoints.dir import DirView
from seahub.api2.endpoints.repo_trash import RepoTrash from seahub.api2.endpoints.repo_trash import RepoTrash
from seahub.api2.endpoints.deleted_repos import DeletedRepos
from seahub.api2.endpoints.repo_history import RepoHistory from seahub.api2.endpoints.repo_history import RepoHistory
from seahub.api2.endpoints.repo_set_password import RepoSetPassword from seahub.api2.endpoints.repo_set_password import RepoSetPassword
from seahub.api2.endpoints.zip_task import ZipTaskView from seahub.api2.endpoints.zip_task import ZipTaskView
@@ -209,6 +210,7 @@ urlpatterns = patterns(
## user::repos ## user::repos
url(r'^api/v2.1/repos/batch/$', ReposBatchView.as_view(), name='api-v2.1-repos-batch'), url(r'^api/v2.1/repos/batch/$', ReposBatchView.as_view(), name='api-v2.1-repos-batch'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'), url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'),
url(r'^api/v2.1/deleted-repos/$', DeletedRepos.as_view(), name='api2-v2.1-deleted-repos'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/$', FileView.as_view(), name='api-v2.1-file-view'), url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/$', FileView.as_view(), name='api-v2.1-file-view'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'), url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'), url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'),

View File

@@ -0,0 +1,18 @@
define([
'underscore',
'backbone',
'common',
'app/models/deleted-repo'
], function(_, Backbone, Common, DeletedRepo) {
'use strict';
var collection = Backbone.Collection.extend({
model: DeletedRepo,
url: function () {
return Common.getUrl({name: 'deleted_repos'});
}
});
return collection;
});

View File

@@ -0,0 +1,30 @@
define([
'underscore',
'backbone',
'common'
], function(_, Backbone, Common) {
'use strict';
var model = Backbone.Model.extend({
getIconUrl: function(size) {
var is_encrypted = this.get('encrypted');
var is_readonly = false;
return Common.getLibIconUrl(is_encrypted, is_readonly, size);
},
getIconTitle: function() {
var icon_title = '';
if (this.get('encrypted')) {
icon_title = gettext("Encrypted library");
} else {
icon_title = gettext("Read-Write library");
}
return icon_title;
}
});
return model;
});

View File

@@ -6,6 +6,7 @@ define([
'js.cookie', 'js.cookie',
'app/views/side-nav', 'app/views/side-nav',
'app/views/myhome-repos', 'app/views/myhome-repos',
'app/views/my-deleted-repos',
'app/views/myhome-shared-repos', 'app/views/myhome-shared-repos',
'app/views/groups', 'app/views/groups',
'app/views/group', 'app/views/group',
@@ -22,8 +23,8 @@ define([
'app/views/notifications', 'app/views/notifications',
'app/views/account' 'app/views/account'
], function($, Backbone, Common, Cookies, SideNavView, MyReposView, ], function($, Backbone, Common, Cookies, SideNavView, MyReposView,
SharedReposView, GroupsView, GroupView, OrgView, DirView, MyDeletedReposView, SharedReposView, GroupsView, GroupView, OrgView,
StarredFileView, ActivitiesView, DevicesView, InvitationsView, DirView, StarredFileView, ActivitiesView, DevicesView, InvitationsView,
ShareAdminReposView, ShareAdminFoldersView, ShareAdminShareLinksView, ShareAdminReposView, ShareAdminFoldersView, ShareAdminShareLinksView,
ShareAdminUploadLinksView, NotificationsView, AccountView) { ShareAdminUploadLinksView, NotificationsView, AccountView) {
"use strict"; "use strict";
@@ -32,6 +33,7 @@ define([
routes: { routes: {
'': 'showRepos', '': 'showRepos',
'my-libs/': 'showMyRepos', 'my-libs/': 'showMyRepos',
'my-libs/deleted/': 'showMyDeletedRepos',
'my-libs/lib/:repo_id(/*path)': 'showMyRepoDir', 'my-libs/lib/:repo_id(/*path)': 'showMyRepoDir',
'shared-libs/': 'showSharedRepos', 'shared-libs/': 'showSharedRepos',
'shared-libs/lib/:repo_id(/*path)': 'showSharedRepoDir', 'shared-libs/lib/:repo_id(/*path)': 'showSharedRepoDir',
@@ -67,6 +69,7 @@ define([
this.dirView = new DirView(); this.dirView = new DirView();
this.myReposView = new MyReposView(); this.myReposView = new MyReposView();
this.myDeletedReposView = new MyDeletedReposView();
this.sharedReposView = new SharedReposView(); this.sharedReposView = new SharedReposView();
this.orgView = new OrgView(); this.orgView = new OrgView();
this.groupView = new GroupView(); this.groupView = new GroupView();
@@ -148,6 +151,12 @@ define([
this.sideNavView.setCurTab('mine'); this.sideNavView.setCurTab('mine');
}, },
showMyDeletedRepos: function() {
this.switchCurrentView(this.myDeletedReposView);
this.myDeletedReposView.show();
this.sideNavView.setCurTab('mine');
},
showSharedRepos: function() { showSharedRepos: function() {
this.switchCurrentView(this.sharedReposView); this.switchCurrentView(this.sharedReposView);
this.sharedReposView.show(); this.sharedReposView.show();

View File

@@ -0,0 +1,72 @@
define([
'jquery',
'underscore',
'backbone',
'common',
'moment',
'app/views/widgets/hl-item-view'
], function($, _, Backbone, Common, Moment, HLItemView) {
'use strict';
var RepoView = HLItemView.extend({
tagName: 'tr',
template: _.template($('#deleted-repo-tmpl').html()),
mobileTemplate: _.template($('#deleted-repo-mobile-tmpl').html()),
events: {
'click .restore': 'restoreRepo'
},
initialize: function() {
HLItemView.prototype.initialize.call(this);
},
render: function() {
var obj = this.model.toJSON();
var icon_size = Common.isHiDPI() ? 96 : 24;
var icon_url = this.model.getIconUrl(icon_size);
var m = Moment(this.model.get('del_time'));
var tmpl;
if ($(window).width() >= 768) {
tmpl = this.template;
} else {
tmpl = this.mobileTemplate;
}
_.extend(obj, {
'icon_url': icon_url,
'icon_title': this.model.getIconTitle(),
'time': m.format('LLLL'),
'time_from_now': Common.getRelativeTimeStr(m)
});
this.$el.html(tmpl(obj));
return this;
},
restoreRepo: function() {
var _this = this;
$.ajax({
url: Common.getUrl({'name': 'deleted_repos'}),
type: 'POST',
data: {
'repo_id': this.model.get('repo_id')
},
beforeSend: Common.prepareCSRFToken,
success: function() {
_this.remove();
var msg = gettext("Successfully restored library {placeholder}").replace('{placeholder}', _this.model.get('repo_name'));
Common.feedback(msg, 'success');
},
error: function(xhr) {
Common.ajaxErrorHandler(xhr);
}
});
return false;
}
});
return RepoView;
});

View File

@@ -0,0 +1,105 @@
define([
'jquery',
'underscore',
'backbone',
'common',
'app/collections/deleted-repos',
'app/views/deleted-repo'
], function($, _, Backbone, Common, RepoCollection, RepoView) {
'use strict';
var ReposView = Backbone.View.extend({
id: "my-deleted-repos",
template: _.template($('#my-deleted-repos-tmpl').html()),
reposHdTemplate: _.template($('#my-deleted-repos-hd-tmpl').html()),
mobileReposHdTemplate: _.template($('#my-deleted-repos-hd-mobile-tmpl').html()),
events: {
},
initialize: function(options) {
this.repos = new RepoCollection();
this.listenTo(this.repos, 'add', this.addOne);
this.listenTo(this.repos, 'reset', this.reset);
this.render();
},
render: function() {
this.$el.html(this.template());
this.$table = this.$('table');
this.$tableHead = $('thead', this.$table);
this.$tableBody = $('tbody', this.$table);
this.$loadingTip = this.$('.loading-tip');
this.$emptyTip = this.$('.empty-tips');
return this;
},
addOne: function(repo, collection, options) {
var view = new RepoView({model: repo});
this.$tableBody.append(view.render().el);
},
renderReposHd: function() {
var tmpl = $(window).width() >= 768 ? this.reposHdTemplate : this.mobileReposHdTemplate;
this.$tableHead.html(tmpl());
},
reset: function() {
this.$('.error').hide();
this.$loadingTip.hide();
if (this.repos.length) {
this.$emptyTip.hide();
this.renderReposHd();
this.$tableBody.empty();
this.repos.each(this.addOne, this);
this.$table.show();
} else {
this.$table.hide();
this.$emptyTip.show();
}
},
showRepos: function() {
this.$table.hide();
this.$loadingTip.show();
var _this = this;
this.repos.fetch({
cache: false,
reset: true,
success: function (collection, response, opts) {
},
error: function (collection, response, opts) {
_this.$loadingTip.hide();
var $error = _this.$('.error');
var err_msg;
if (response.responseText) {
if (response['status'] == 401 || response['status'] == 403) {
err_msg = gettext("Permission error");
} else {
err_msg = gettext("Error");
}
} else {
err_msg = gettext('Please check the network.');
}
$error.html(err_msg).show();
}
});
},
show: function() {
$("#right-panel").html(this.$el);
this.showRepos();
},
hide: function() {
this.$el.detach();
}
});
return ReposView;
});

View File

@@ -6,9 +6,10 @@ define([
'app/collections/repos', 'app/collections/repos',
'app/views/repo', 'app/views/repo',
'app/views/add-repo', 'app/views/add-repo',
'app/views/repo-details' 'app/views/repo-details',
'app/views/widgets/dropdown'
], function($, _, Backbone, Common, RepoCollection, RepoView, AddRepoView, ], function($, _, Backbone, Common, RepoCollection, RepoView, AddRepoView,
RepoDetailsView) { RepoDetailsView, DropdownView) {
'use strict'; 'use strict';
var ReposView = Backbone.View.extend({ var ReposView = Backbone.View.extend({
@@ -21,7 +22,8 @@ define([
events: { events: {
'click .repo-create': 'createRepo', 'click .repo-create': 'createRepo',
'click .by-name': 'sortByName', 'click .by-name': 'sortByName',
'click .by-time': 'sortByTime' 'click .by-time': 'sortByTime',
'click #my-libs-more-op a': 'closeDropdown'
}, },
initialize: function(options) { initialize: function(options) {
@@ -32,6 +34,11 @@ define([
this.repoDetailsView = new RepoDetailsView(); this.repoDetailsView = new RepoDetailsView();
this.render(); this.render();
this.more_op_dropdown = new DropdownView({
el: this.$("#my-libs-more-op"),
right: 0
})
}, },
addOne: function(repo, collection, options) { addOne: function(repo, collection, options) {
@@ -149,6 +156,10 @@ define([
this.repos.comparator = null; this.repos.comparator = null;
return false; return false;
},
closeDropdown: function() {
this.more_op_dropdown.hide();
} }
}); });

View File

@@ -98,6 +98,7 @@ define([
// Repos // Repos
case 'repos': return siteRoot + 'api2/repos/'; case 'repos': return siteRoot + 'api2/repos/';
case 'deleted_repos': return siteRoot + 'api/v2.1/deleted-repos/';
case 'pub_repos': return siteRoot + 'api2/repos/public/'; case 'pub_repos': return siteRoot + 'api2/repos/public/';
case 'unenc_rw_repos': return siteRoot + 'ajax/unenc-rw-repos/'; case 'unenc_rw_repos': return siteRoot + 'ajax/unenc-rw-repos/';
case 'api_v2.1_repo_set_password': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/set-password/'; case 'api_v2.1_repo_set_password': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/set-password/';

View File

@@ -0,0 +1,47 @@
import json
from django.core.urlresolvers import reverse
from seaserv import seafile_api
from seahub.test_utils import BaseTestCase
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
class DeletedReposTest(BaseTestCase):
def test_get_deleted_repos(self):
self.login_as(self.user)
name = self.user.username
repoid = self.create_repo(name='test-repo', desc='',
username=name,
passwd=None)
repo = seafile_api.get_repo(repoid)
self.remove_repo(repoid)
trashs = self.client.get(reverse("api2-v2.1-deleted-repos"))
json_trashs = json.loads(trashs.content)
json_trashs = [trash for trash in json_trashs if trash['repo_id'] == repo.id]
assert json_trashs[0]['repo_id'] == repo.id
assert json_trashs[0]['owner_email'] == name
assert json_trashs[0]['owner_name'] == email2nickname(name)
assert json_trashs[0]['owner_contact_email'] == email2contact_email(name)
assert json_trashs[0]['repo_name'] == repo.name
#assert json_trashs[0]['org_id'] == repo.org_id
assert json_trashs[0]['size'] == repo.size
self.assertIsNotNone(json_trashs[0]['head_commit_id'])
self.assertIsNotNone(json_trashs[0]['del_time'])
#self.assertIsNotNone(json_trashs[0]['encrypted'])
def test_can_restore_deleted_repos(self):
self.login_as(self.user)
name = self.user.username
repoid = self.create_repo(name='test-repo', desc='',
username=name,
passwd=None)
remove_status = self.remove_repo(repoid)
assert remove_status == 0
response = self.client.post(
reverse("api2-v2.1-deleted-repos"),
{"repo_id": repoid}
)
self.assertEqual(response.status_code, 200)