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 %} - - - - - - - - {% for group in groups %} - - - - - - - {% endfor %} -
    {% trans "Name" %}{% trans "Creator" %}{% trans "Create At" %}{% trans "Operations" %}
    - {% if group.org_id %} - {{ group.props.group_name }} -

    ({{group.org_name}})

    - {% else %} - {{ group.props.group_name }} - {% endif %} -
    {{ group.props.creator_name }}{{ group.props.timestamp|tsstr_sec }}{% trans "Delete" %}
    - -{% 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)