diff --git a/seahub/api2/endpoints/admin/groups.py b/seahub/api2/endpoints/admin/groups.py
new file mode 100644
index 0000000000..bb129b48e5
--- /dev/null
+++ b/seahub/api2/endpoints/admin/groups.py
@@ -0,0 +1,167 @@
+import logging
+
+from django.utils.translation import ugettext as _
+
+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 seaserv import seafile_api, ccnet_api
+from pysearpc import SearpcError
+
+from seahub.base.accounts import User
+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, \
+ is_group_owner
+
+from seahub.api2.utils import api_error
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.authentication import TokenAuthentication
+
+logger = logging.getLogger(__name__)
+
+def get_group_info(group_id):
+ group = ccnet_api.get_group(group_id)
+ isoformat_timestr = timestamp_to_isoformat_timestr(group.timestamp)
+ group_info = {
+ "id": group.id,
+ "name": group.group_name,
+ "owner": group.creator_name,
+ "created_at": isoformat_timestr,
+ }
+
+ return group_info
+
+class AdminGroups(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsAdminUser,)
+
+ def get(self, request):
+ """ List all groups
+
+ Permission checking:
+ 1. Admin user;
+ """
+
+ 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 = (current_page - 1) * per_page
+ limit = per_page + 1
+
+ groups_all = ccnet_api.get_all_groups(start, limit)
+
+ if len(groups_all) > per_page:
+ groups_all = groups_all[:per_page]
+ has_next_page = True
+ else:
+ has_next_page = False
+
+ return_results = []
+
+ for group in groups_all:
+ group_info = get_group_info(group.id)
+ return_results.append(group_info)
+
+ page_info = {
+ 'has_next_page': has_next_page,
+ 'current_page': current_page
+ }
+
+ return Response({"page_info": page_info, "groups": return_results})
+
+
+class AdminGroup(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsAdminUser,)
+
+ def put(self, request, group_id):
+ """ Admin transfer a group
+
+ Permission checking:
+ 1. Admin user;
+ """
+
+ # argument check
+ new_owner = request.data.get('new_owner', None)
+ if not new_owner or not is_valid_username(new_owner):
+ error_msg = 'new_owner %s invalid.' % new_owner
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ old_owner = request.data.get('old_owner', None)
+ if not old_owner or not is_valid_username(old_owner):
+ error_msg = 'old_owner %s invalid.' % old_owner
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # recourse check
+ group_id = int(group_id) # Checked by URL Conf
+ group = ccnet_api.get_group(group_id)
+ if not group:
+ error_msg = 'Group %d not found.' % group_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # check if new_owner exists,
+ # NOT need to check old_owner for old_owner may has been deleted.
+ try:
+ User.objects.get(email=new_owner)
+ except User.DoesNotExist:
+ error_msg = 'User %s not found.' % new_owner
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if new_owner == old_owner:
+ error_msg = 'new_owner %s is the same as old_owner %s.' % \
+ (new_owner, old_owner)
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not is_group_owner(group_id, old_owner):
+ error_msg = _(u'User %s is not group owner.') % old_owner
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if is_group_owner(group_id, new_owner):
+ error_msg = _(u'User %s is already group owner.') % new_owner
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # transfer a group
+ try:
+ if not is_group_member(group_id, new_owner):
+ ccnet_api.group_add_member(group_id, old_owner, new_owner)
+
+ if not is_group_admin(group_id, new_owner):
+ ccnet_api.group_set_admin(group_id, new_owner)
+
+ ccnet_api.set_group_creator(group_id, new_owner)
+ ccnet_api.group_unset_admin(group_id, old_owner)
+ except SearpcError as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ group_info = get_group_info(group_id)
+
+ return Response(group_info)
+
+ def delete(self, request, group_id):
+ """ Dismiss a specific group
+ """
+
+ try:
+ group_id = int(group_id)
+ ccnet_api.remove_group(group_id)
+ seafile_api.remove_group_repos(group_id)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
diff --git a/seahub/templates/js/sysadmin-templates.html b/seahub/templates/js/sysadmin-templates.html
index 7fd2dec1b6..4af647d889 100644
--- a/seahub/templates/js/sysadmin-templates.html
+++ b/seahub/templates/js/sysadmin-templates.html
@@ -20,8 +20,8 @@
{% trans "Users" %}
-
- {% trans "Groups" %}
+
+ {% trans "Groups" %}
{% if multi_tenancy %}
@@ -310,6 +310,7 @@
{% trans "No libraries" %}
+
-
+
+
+
+
+
+
diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html
index 9f7f51fba7..28bbf26dad 100644
--- a/seahub/templates/sysadmin/base.html
+++ b/seahub/templates/sysadmin/base.html
@@ -29,7 +29,7 @@
{% trans "Users" %}
- {% trans "Groups" %}
+ {% trans "Groups" %}
{% if multi_tenancy %}
diff --git a/seahub/templates/sysadmin/sys_group_admin.html b/seahub/templates/sysadmin/sys_group_admin.html
deleted file mode 100644
index 67c6c099d5..0000000000
--- a/seahub/templates/sysadmin/sys_group_admin.html
+++ /dev/null
@@ -1,59 +0,0 @@
-{% extends "sysadmin/base.html" %}
-{% load seahub_tags i18n %}
-
-{% block cur_groups %}tab-cur{% endblock %}
-
-{% block right_panel %}
-
-
{% trans "All Groups"%}
- {% if groups %}
-
- {% endif %}
-
-
-{% if groups %}
-
-
-{% include "snippets/admin_paginator.html" %}
-
-{% else %}
-{% trans "Empty" %}
-{% endif %}
-{% endblock %}
-
-{% block extra_script %}
-
-{% endblock %}
diff --git a/seahub/urls.py b/seahub/urls.py
index 42af9ab014..6bd1c52acd 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -30,6 +30,9 @@ from seahub.api2.endpoints.dirents_download_link import DirentsDownloadLinkView
from seahub.api2.endpoints.zip_task import ZipTaskView
from seahub.api2.endpoints.share_link_zip_task import ShareLinkZipTaskView
from seahub.api2.endpoints.query_zip_progress import QueryZipProgressView
+from seahub.api2.endpoints.invitations import InvitationsView
+from seahub.api2.endpoints.invitation import InvitationView
+
from seahub.api2.endpoints.admin.login import Login
from seahub.api2.endpoints.admin.file_audit import FileAudit
from seahub.api2.endpoints.admin.file_update import FileUpdate
@@ -41,8 +44,7 @@ from seahub.api2.endpoints.admin.libraries import AdminLibraries, AdminLibrary
from seahub.api2.endpoints.admin.library_dirents import AdminLibraryDirents, AdminLibraryDirent
from seahub.api2.endpoints.admin.system_library import AdminSystemLibrary
from seahub.api2.endpoints.admin.trash_libraries import AdminTrashLibraries, AdminTrashLibrary
-from seahub.api2.endpoints.invitations import InvitationsView
-from seahub.api2.endpoints.invitation import InvitationView
+from seahub.api2.endpoints.admin.groups import AdminGroups, AdminGroup
# Uncomment the next two lines to enable the admin:
#from django.contrib import admin
@@ -206,6 +208,8 @@ urlpatterns = patterns(
url(r'^api/v2.1/admin/libraries/$', AdminLibraries.as_view(), name='api-v2.1-admin-libraries'),
url(r'^api/v2.1/admin/libraries/(?P[-0-9a-f]{36})/$', AdminLibrary.as_view(), name='api-v2.1-admin-library'),
url(r'^api/v2.1/admin/libraries/(?P[-0-9a-f]{36})/dirents/$', AdminLibraryDirents.as_view(), name='api-v2.1-admin-library-dirents'),
+ url(r'^api/v2.1/admin/groups/$', AdminGroups.as_view(), name='api-v2.1-admin-groups'),
+ url(r'^api/v2.1/admin/groups/(?P\d+)/$', AdminGroup.as_view(), name='api-v2.1-admin-group'),
url(r'^api/v2.1/admin/libraries/(?P[-0-9a-f]{36})/dirent/$', AdminLibraryDirent.as_view(), name='api-v2.1-admin-library-dirent'),
url(r'^api/v2.1/admin/system-library/$', AdminSystemLibrary.as_view(), name='api-v2.1-admin-system-library'),
url(r'^api/v2.1/admin/trash-libraries/$', AdminTrashLibraries.as_view(), name='api-v2.1-admin-trash-libraries'),
@@ -242,7 +246,6 @@ urlpatterns = patterns(
url(r'^sys/useradmin/ldap/$', sys_user_admin_ldap, name='sys_useradmin_ldap'),
url(r'^sys/useradmin/ldap/imported$', sys_user_admin_ldap_imported, name='sys_useradmin_ldap_imported'),
url(r'^sys/useradmin/admins/$', sys_user_admin_admins, name='sys_useradmin_admins'),
- url(r'^sys/groupadmin/$', sys_group_admin, name='sys_group_admin'),
url(r'^sys/groupadmin/export-excel/$', sys_group_admin_export_excel, name='sys_group_admin_export_excel'),
url(r'^sys/groupadmin/(?P\d+)/$', sys_admin_group_info, name='sys_admin_group_info'),
url(r'^sys/orgadmin/$', sys_org_admin, name='sys_org_admin'),
diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py
index bc002a452f..f5a00708ef 100644
--- a/seahub/views/sysadmin.py
+++ b/seahub/views/sysadmin.py
@@ -1055,41 +1055,6 @@ def user_add(request):
else:
return HttpResponse(json.dumps({'error': str(form.errors.values()[0])}), status=400, content_type=content_type)
-@login_required
-@sys_staff_required
-def sys_group_admin(request):
- # 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', '25'))
- except ValueError:
- current_page = 1
- per_page = 25
-
- groups_plus_one = ccnet_threaded_rpc.get_all_groups(per_page * (current_page -1),
- per_page +1)
-
- groups = groups_plus_one[:per_page]
- for grp in groups:
- org_id = ccnet_threaded_rpc.get_org_id_by_group(int(grp.id))
- if org_id > 0:
- grp.org_id = org_id
- grp.org_name = ccnet_threaded_rpc.get_org_by_id(int(org_id)).org_name
-
- if len(groups_plus_one) == per_page + 1:
- page_next = True
- else:
- page_next = False
-
- return render_to_response('sysadmin/sys_group_admin.html', {
- 'groups': groups,
- '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))
-
@login_required
@sys_staff_required
def sys_group_admin_export_excel(request):
diff --git a/static/scripts/common.js b/static/scripts/common.js
index 03095b1b9b..fe8777571f 100644
--- a/static/scripts/common.js
+++ b/static/scripts/common.js
@@ -175,6 +175,8 @@ define([
case 'admin-libraries': return siteRoot + 'api/v2.1/admin/libraries/';
case 'admin-library': return siteRoot + 'api/v2.1/admin/libraries/' + options.repo_id + '/';
case 'admin-library-dirents': return siteRoot + 'api/v2.1/admin/libraries/' + options.repo_id + '/dirents/';
+ case 'admin-groups': return siteRoot + 'api/v2.1/admin/groups/';
+ case 'admin-group': return siteRoot + 'api/v2.1/admin/groups/' + options.group_id + '/';
case 'admin-system-library': return siteRoot + 'api/v2.1/admin/system-library/';
case 'admin-trash-libraries': return siteRoot + 'api/v2.1/admin/trash-libraries/';
case 'admin-trash-library': return siteRoot + 'api/v2.1/admin/trash-libraries/' + options.repo_id + '/';
diff --git a/static/scripts/sysadmin-app/collection/groups.js b/static/scripts/sysadmin-app/collection/groups.js
new file mode 100644
index 0000000000..3f7b135557
--- /dev/null
+++ b/static/scripts/sysadmin-app/collection/groups.js
@@ -0,0 +1,24 @@
+define([
+ 'underscore',
+ 'backbone.paginator',
+ 'common',
+ 'sysadmin-app/models/group'
+], function(_, BackbonePaginator, Common, GroupModel) {
+ 'use strict';
+
+ var GroupCollection = Backbone.PageableCollection.extend({
+ model: GroupModel,
+ state: {pageSize: 100},
+ parseState: function(data) {
+ return data.page_info; // {'has_next_page': has_next_page, 'current_page': current_page}
+ },
+ parseRecords: function(data) {
+ return data.groups;
+ },
+ url: function () {
+ return Common.getUrl({name: 'admin-groups'});
+ }
+ });
+
+ return GroupCollection;
+});
diff --git a/static/scripts/sysadmin-app/models/group.js b/static/scripts/sysadmin-app/models/group.js
new file mode 100644
index 0000000000..b47795115e
--- /dev/null
+++ b/static/scripts/sysadmin-app/models/group.js
@@ -0,0 +1,11 @@
+define([
+ 'underscore',
+ 'backbone',
+ 'common',
+], function(_, Backbone, Common) {
+ 'use strict';
+
+ var GroupModel = Backbone.Model.extend({});
+
+ return GroupModel;
+});
diff --git a/static/scripts/sysadmin-app/router.js b/static/scripts/sysadmin-app/router.js
index 3416f1d219..3f554cc6c2 100644
--- a/static/scripts/sysadmin-app/router.js
+++ b/static/scripts/sysadmin-app/router.js
@@ -14,11 +14,12 @@ define([
'sysadmin-app/views/trash-repos',
'sysadmin-app/views/search-trash-repos',
'sysadmin-app/views/dir',
+ 'sysadmin-app/views/groups',
'app/views/account'
], function($, Backbone, Common, SideNavView, DashboardView,
DesktopDevicesView, MobileDevicesView, DeviceErrorsView,
- ReposView, SearchReposView, SystemReposView, TrashReposView,
- SearchTrashReposView, DirView, AccountView) {
+ ReposView, SearchReposView, SystemReposView, TrashReposView,
+ SearchTrashReposView, DirView, GroupsView, AccountView) {
"use strict";
@@ -35,6 +36,7 @@ define([
'trash-libs/': 'showTrashLibraries',
'search-trash-libs/': 'showSearchTrashLibraries',
'libs/:repo_id(/*path)': 'showLibraryDir',
+ 'groups/': 'showGroups',
// Default
'*actions': 'showDashboard'
},
@@ -62,6 +64,8 @@ define([
this.searchTrashReposView = new SearchTrashReposView();
this.dirView = new DirView();
+ this.groupsView = new GroupsView();
+
app.ui.accountView = this.accountView = new AccountView();
this.currentView = this.dashboardView;
@@ -115,7 +119,7 @@ define([
},
showLibraries: function() {
- // url_match: null or an array like ["http://127.0.0.1:8000/sysadmin/#libraries/?page=2", "2"]
+ // url_match: null or an array like ["http://127.0.0.1:8000/sysadmin/#libraries/?page=2", "2"]
var url_match = location.href.match(/.*?page=(\d+)/);
var page = url_match ? url_match[1] : 1; // 1: default
@@ -175,6 +179,16 @@ define([
this.switchCurrentView(this.searchTrashReposView);
this.sideNavView.setCurTab('libraries', {'option': 'trash'});
this.searchTrashReposView.show({'owner': decodeURIComponent(owner)});
+ },
+
+ showGroups: function() {
+ // url_match: null or an array like ["http://127.0.0.1:8000/sysadmin/#groups/?page=2", "2"]
+ var url_match = location.href.match(/.*?page=(\d+)/);
+ var page = url_match ? url_match[1] : 1; // 1: default
+
+ this.switchCurrentView(this.groupsView);
+ this.sideNavView.setCurTab('groups');
+ this.groupsView.show({'page': page});
}
});
diff --git a/static/scripts/sysadmin-app/views/group.js b/static/scripts/sysadmin-app/views/group.js
new file mode 100644
index 0000000000..45c9a16008
--- /dev/null
+++ b/static/scripts/sysadmin-app/views/group.js
@@ -0,0 +1,136 @@
+define([
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'common',
+ 'moment',
+ 'simplemodal',
+ 'select2',
+ 'app/views/widgets/hl-item-view'
+], function($, _, Backbone, Common, Moment, Simplemodal, Select2, HLItemView) {
+ 'use strict';
+
+ var GroupView = HLItemView.extend({
+ tagName: 'tr',
+
+ template: _.template($('#group-item-tmpl').html()),
+ transferTemplate: _.template($('#group-transfer-form-tmpl').html()),
+
+ events: {
+ 'click .group-delete-btn': 'deleteGroup',
+ 'click .group-transfer-btn': 'transferGroup'
+ },
+
+ initialize: function() {
+ HLItemView.prototype.initialize.call(this);
+ this.listenTo(this.model, "change", this.render);
+ },
+
+ deleteGroup: function() {
+ var _this = this;
+ var group_name = this.model.get('name');
+ var popupTitle = gettext("Delete Group");
+ var popupContent = gettext("Are you sure you want to delete %s ?").replace('%s', '' + Common.HTMLescape(group_name) + '');
+ var yesCallback = function() {
+ $.ajax({
+ url: Common.getUrl({
+ 'name':'admin-group',
+ 'group_id': _this.model.get('id')
+ }),
+ type: 'DELETE',
+ cache: false,
+ beforeSend: Common.prepareCSRFToken,
+ dataType: 'json',
+ success: function() {
+ _this.$el.remove();
+ Common.feedback(gettext("Successfully deleted."), 'success');
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ Common.ajaxErrorHandler(xhr, textStatus, errorThrown);
+ },
+ complete: function() {
+ $.modal.close();
+ }
+ });
+ };
+ Common.showConfirm(popupTitle, popupContent, yesCallback);
+ return false;
+ },
+
+ transferGroup: function() {
+ var _this = this;
+ var group_name = this.model.get('name');
+ var $form = $(this.transferTemplate({
+ title: gettext("Transfer Group {group_name} To").replace('{group_name}',
+ '' + Common.HTMLescape(group_name) + '')
+ }));
+
+ $form.modal({focus:false});
+ $('#simplemodal-container').css({'width':'auto', 'height':'auto'});
+ $('[name="email"]', $form).select2($.extend(
+ Common.contactInputOptionsForSelect2(), {
+ width: '300px',
+ maximumSelectionSize: 1,
+ placeholder: gettext("Search user or enter email and press Enter"), // to override 'placeholder' returned by `Common.conta...`
+ formatSelectionTooBig: gettext("You cannot select any more choices")
+ }));
+
+ $form.submit(function() {
+ var email = $.trim($('[name="email"]', $(this)).val());
+ if (!email) {
+ return false;
+ }
+ if (email == _this.model.get('owner')) {
+ return false;
+ }
+
+ var url = Common.getUrl({'name': 'admin-group','group_id': _this.model.get('id')});
+ var $submitBtn = $('[type="submit"]', $(this));
+ Common.disableButton($submitBtn);
+
+ $.ajax({
+ url: url,
+ type: 'put',
+ dataType: 'json',
+ beforeSend: Common.prepareCSRFToken,
+ data: {
+ 'new_owner': email,
+ 'old_owner': _this.model.get('owner')
+ },
+ success: function() {
+ $.modal.close();
+ _this.model.set({'owner': email}); // it will trigger 'change' event
+ Common.feedback(gettext("Successfully transferred the group."), 'success');
+ },
+ error: function(xhr) {
+ var error_msg;
+ if (xhr.responseText) {
+ error_msg = $.parseJSON(xhr.responseText).error_msg;
+ } else {
+ error_msg = gettext("Failed. Please check the network.");
+ }
+ $('.error', $form).html(error_msg).show();
+ Common.enableButton($submitBtn);
+ }
+ });
+ return false;
+ });
+ return false;
+ },
+
+ render: function() {
+ var data = this.model.toJSON(),
+ created_at = Moment(data['created_at']);
+
+ data['time'] = created_at.format('LLLL');
+ data['time_from_now'] = Common.getRelativeTimeStr(created_at);
+
+ this.$el.html(this.template(data));
+
+ return this;
+ }
+
+ });
+
+ return GroupView;
+});
diff --git a/static/scripts/sysadmin-app/views/groups.js b/static/scripts/sysadmin-app/views/groups.js
new file mode 100644
index 0000000000..0c863bd418
--- /dev/null
+++ b/static/scripts/sysadmin-app/views/groups.js
@@ -0,0 +1,155 @@
+define([
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'common',
+ 'sysadmin-app/views/group',
+ 'sysadmin-app/collection/groups'
+], function($, _, Backbone, Common, GroupView, GroupCollection) {
+ 'use strict';
+
+ var GroupsView = Backbone.View.extend({
+
+ id: 'admin-groups',
+
+ template: _.template($("#groups-tmpl").html()),
+
+ initialize: function() {
+ this.groupCollection = new GroupCollection();
+ this.listenTo(this.groupCollection, 'add', this.addOne);
+ this.listenTo(this.groupCollection, '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 .js-export-excel': 'exportExcel',
+ 'click #paginator .js-next': 'getNextPage',
+ 'click #paginator .js-previous': 'getPreviousPage'
+ },
+
+ exportExcel: function() {
+ location.href = app.config.siteRoot + "sys/groupadmin/export-excel/";
+ },
+
+ initPage: function() {
+ this.$table.hide();
+ this.$tableBody.empty();
+ this.$loadingTip.show();
+ this.$emptyTip.hide();
+ this.$jsNext.hide();
+ this.$jsPrevious.hide();
+ },
+
+ getNextPage: function() {
+ this.initPage();
+ var current_page = this.groupCollection.state.current_page;
+ if (this.groupCollection.state.has_next_page) {
+ this.groupCollection.getPage(current_page + 1, {
+ reset: true
+ });
+ }
+
+ return false;
+ },
+
+ getPreviousPage: function() {
+ this.initPage();
+ var current_page = this.groupCollection.state.current_page;
+ if (current_page > 1) {
+ this.groupCollection.getPage(current_page - 1, {
+ reset: true
+ });
+ }
+ return false;
+ },
+
+ 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();
+ },
+
+ getContent: function() {
+ this.initPage();
+ var _this = this;
+ this.groupCollection.fetch({
+ data: {'page': this.option.page},
+ 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');
+ },
+ complete:function() {
+ _this.$loadingTip.hide();
+ }
+ });
+ },
+
+ reset: function() {
+ // update the url
+ var current_page = this.groupCollection.state.current_page;
+ app.router.navigate('groups/?page=' + current_page);
+
+ this.$loadingTip.hide();
+ if (this.groupCollection.length > 0) {
+ this.groupCollection.each(this.addOne, this);
+ this.$table.show();
+ this.renderPaginator();
+ } else {
+ this.$emptyTip.show();
+ }
+ },
+
+ renderPaginator: function() {
+ if (this.groupCollection.state.has_next_page) {
+ this.$jsNext.show();
+ } else {
+ this.$jsNext.hide();
+ }
+
+ var current_page = this.groupCollection.state.current_page;
+ if (current_page > 1) {
+ this.$jsPrevious.show();
+ } else {
+ this.$jsPrevious.hide();
+ }
+ },
+
+ addOne: function(group) {
+ var view = new GroupView({model: group});
+ this.$tableBody.append(view.render().el);
+ }
+ });
+
+ return GroupsView;
+
+});
diff --git a/tests/api/endpoints/admin/test_groups.py b/tests/api/endpoints/admin/test_groups.py
new file mode 100644
index 0000000000..686c9fa7e0
--- /dev/null
+++ b/tests/api/endpoints/admin/test_groups.py
@@ -0,0 +1,104 @@
+import json
+from django.core.urlresolvers import reverse
+from seahub.test_utils import BaseTestCase
+
+class GroupsTest(BaseTestCase):
+
+ def setUp(self):
+ self.user_name = self.user.username
+ self.admin_name = self.admin.username
+
+ def tearDown(self):
+ self.remove_group()
+
+ def test_can_get(self):
+ self.login_as(self.admin)
+ url = reverse('api-v2.1-admin-groups')
+ resp = self.client.get(url)
+
+ json_resp = json.loads(resp.content)
+ assert len(json_resp['groups']) > 0
+
+ def test_get_with_invalid_user_permission(self):
+ self.login_as(self.user)
+ url = reverse('api-v2.1-admin-groups')
+ resp = self.client.get(url)
+ self.assertEqual(403, resp.status_code)
+
+class GroupTest(BaseTestCase):
+
+ def setUp(self):
+ self.user_name = self.user.username
+ self.admin_name = self.admin.username
+ self.group_id = self.group.id
+
+ def test_can_transfer_group(self):
+
+ self.login_as(self.admin)
+
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'old_owner=%s&new_owner=%s' % (self.user_name, self.admin_name)
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+
+ self.assertEqual(200, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert json_resp['owner'] == self.admin_name
+
+ def test_transfer_group_invalid_user_permission(self):
+
+ self.login_as(self.user)
+
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'old_owner=%s&new_owner=%s' % (self.user_name, self.admin_name)
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+
+ self.assertEqual(403, resp.status_code)
+
+ def test_transfer_group_invalid_args(self):
+
+ self.login_as(self.admin)
+
+ # invalid old owner
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'invalid_old_owner=%s&new_owner=%s' % (self.user_name, self.admin_name)
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+ self.assertEqual(400, resp.status_code)
+
+ # invalid new owner
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'old_owner=%s&invalid_new_owner=%s' % (self.user_name, self.admin_name)
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+ self.assertEqual(400, resp.status_code)
+
+ # new_owner is the same as old_owner
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'old_owner=%s&new_owner=%s' % (self.user_name, self.user_name)
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+ self.assertEqual(400, resp.status_code)
+
+ # old_owner is not group owner.
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'old_owner=%s&new_owner=%s' % (self.admin_name, self.admin_name)
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+ self.assertEqual(400, resp.status_code)
+
+ # new owner not exist
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ data = 'old_owner=%s&new_owner=%s' % (self.user_name, 'invalid@user.com')
+ resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
+ self.assertEqual(404, resp.status_code)
+
+ def test_can_delete(self):
+ self.login_as(self.admin)
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ resp = self.client.delete(url)
+ self.assertEqual(200, resp.status_code)
+
+ json_resp = json.loads(resp.content)
+ assert json_resp['success'] is True
+
+ def test_delete_with_invalid_user_permission(self):
+ self.login_as(self.user)
+ url = reverse('api-v2.1-admin-group', args=[self.group_id])
+ resp = self.client.delete(url)
+ self.assertEqual(403, resp.status_code)