diff --git a/media/img/favicon.ico b/media/img/favicon.ico new file mode 100644 index 0000000000..23d8953706 Binary files /dev/null and b/media/img/favicon.ico differ diff --git a/media/img/favicon.png b/media/img/favicon.png deleted file mode 100644 index 379988d7e8..0000000000 Binary files a/media/img/favicon.png and /dev/null differ diff --git a/seahub/api2/endpoints/admin/default_library.py b/seahub/api2/endpoints/admin/default_library.py new file mode 100644 index 0000000000..bfa429a8ee --- /dev/null +++ b/seahub/api2/endpoints/admin/default_library.py @@ -0,0 +1,132 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import logging + +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 django.utils.translation import ugettext as _ + +from seaserv import seafile_api + +from seahub.options.models import UserOptions +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.base.accounts import User +from seahub.views import get_system_default_repo_id + +logger = logging.getLogger(__name__) + + +class AdminDefaultLibrary(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def create_default_repo(self, username): + + default_repo_id = seafile_api.create_repo(name=_("My Library"), + desc=_("My Library"), username=username, passwd=None) + + sys_repo_id = get_system_default_repo_id() + if not sys_repo_id or not seafile_api.get_repo(sys_repo_id): + return None + + dirents = seafile_api.list_dir_by_path(sys_repo_id, '/') + for dirent in dirents: + obj_name = dirent.obj_name + seafile_api.copy_file(sys_repo_id, '/', obj_name, + default_repo_id, '/', obj_name, username, 0) + + UserOptions.objects.set_default_repo(username, default_repo_id) + + return default_repo_id + + def get(self, request): + """ Get info of common user's default library. + + Permission checking: + 1. only admin can perform this action. + """ + + # argument check + user_email = request.GET.get('user_email', None) + if not user_email: + error_msg = 'user_email invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + User.objects.get(email=user_email) + except User.DoesNotExist: + error_msg = 'User %s not found.' % user_email + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # get default library info + try: + default_repo_id = UserOptions.objects.get_default_repo(user_email) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + default_repo_info = {} + default_repo_info['user_email'] = user_email + if default_repo_id and seafile_api.get_repo(default_repo_id) is not None: + default_repo_info['exists'] = True + default_repo_info['repo_id'] = default_repo_id + else: + default_repo_info['exists'] = False + + return Response(default_repo_info) + + def post(self, request): + """ Create a default library for a common user. + + Permission checking: + 1. only admin can perform this action. + """ + + # argument check + user_email = request.POST.get('user_email', None) + if not user_email: + error_msg = 'user_email invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + common_user = User.objects.get(email=user_email) + except User.DoesNotExist: + error_msg = 'User %s not found.' % user_email + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if not common_user.permissions.can_add_repo(): + error_msg = 'Permission denied, %s can not create library.' % user_email + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # create default library for common use + try: + default_repo_id = UserOptions.objects.get_default_repo(user_email) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + default_repo_info = {} + default_repo_info['user_email'] = user_email + default_repo_info['exists'] = True + + try: + if default_repo_id and seafile_api.get_repo(default_repo_id) is not None: + default_repo_info['repo_id'] = default_repo_id + else: + new_default_repo_id = self.create_default_repo(user_email) + default_repo_info['repo_id'] = new_default_repo_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(default_repo_info) diff --git a/seahub/api2/endpoints/admin/devices.py b/seahub/api2/endpoints/admin/devices.py index c081054fe5..ba3e285b11 100644 --- a/seahub/api2/endpoints/admin/devices.py +++ b/seahub/api2/endpoints/admin/devices.py @@ -84,7 +84,7 @@ class AdminDevices(APIView): error_msg = 'device_id invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - if not user or not is_registered_user(user): + if not user: error_msg = 'user invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) @@ -98,4 +98,3 @@ class AdminDevices(APIView): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return Response({'success': True}) - diff --git a/seahub/api2/endpoints/admin/libraries.py b/seahub/api2/endpoints/admin/libraries.py index 51ecd92a1e..8531e92ce2 100644 --- a/seahub/api2/endpoints/admin/libraries.py +++ b/seahub/api2/endpoints/admin/libraries.py @@ -28,11 +28,13 @@ except ImportError: logger = logging.getLogger(__name__) def get_repo_info(repo): + repo_owner = seafile_api.get_repo_owner(repo.repo_id) + org_repo_owner = seafile_api.get_org_repo_owner(repo.repo_id) result = {} result['id'] = repo.repo_id result['name'] = repo.repo_name - result['owner'] = seafile_api.get_repo_owner(repo.repo_id) + result['owner'] = repo_owner or org_repo_owner result['size'] = repo.size result['size_formatted'] = filesizeformat(repo.size) result['encrypted'] = repo.encrypted @@ -149,8 +151,17 @@ class AdminLibrary(APIView): repo = seafile_api.get_repo(repo_id) if not repo: - return api_error(status.HTTP_404_NOT_FOUND, - 'Library %s not found.' % repo_id) + # for case of `seafile-data` has been damaged + # no `repo object` will be returned from seafile api + # delete the database record anyway + try: + seafile_api.remove_repo(repo_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}) repo_name = repo.name repo_owner = seafile_api.get_repo_owner(repo_id) diff --git a/seahub/api2/endpoints/admin/org_users.py b/seahub/api2/endpoints/admin/org_users.py new file mode 100644 index 0000000000..37440fc58c --- /dev/null +++ b/seahub/api2/endpoints/admin/org_users.py @@ -0,0 +1,239 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import logging + +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 constance import config +from seaserv import ccnet_api + +from seahub.utils import clear_token, is_valid_email +from seahub.utils.licenseparse import user_number_over_limit +from seahub.base.accounts import User +from seahub.base.templatetags.seahub_tags import email2nickname +from seahub.profile.models import Profile +from seahub.options.models import UserOptions +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.api2.permissions import IsProVersion + +try: + from seahub.settings import ORG_MEMBER_QUOTA_ENABLED +except ImportError: + ORG_MEMBER_QUOTA_ENABLED= False + +logger = logging.getLogger(__name__) + +def get_org_user_info(org_id, user_obj): + user_info = {} + email = user_obj.username + user_info['org_id'] = org_id + user_info['active'] = user_obj.is_active + user_info['email'] = email + user_info['name'] = email2nickname(email) + user_info['contact_email'] = Profile.objects.get_contact_email_by_user(email) + + return user_info + +class AdminOrgUsers(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser, IsProVersion) + + def post(self, request, org_id): + """ Add new user to org. + + Permission checking: + 1. only admin can perform this action. + """ + # argument check + org_id = int(org_id) + if org_id == 0: + error_msg = 'org_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + org = ccnet_api.get_org_by_id(org_id) + if not org: + error_msg = 'Organization %d not found.' % org_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + email = request.POST.get('email', None) + if not email or not is_valid_email(email): + error_msg = 'email invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + password = request.POST.get('password', None) + if not password: + error_msg = 'password invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + User.objects.get(email=email) + user_exists = True + except User.DoesNotExist: + user_exists = False + + if user_exists: + error_msg = 'User %s already exists.' % email + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # check user number limit by license + if user_number_over_limit(): + error_msg = 'The number of users exceeds the limit.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # check user number limit by org member quota + org_members = len(ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)) + if ORG_MEMBER_QUOTA_ENABLED: + from seahub_extra.organizations.models import OrgMemberQuota + org_members_quota = OrgMemberQuota.objects.get_quota(org_id) + if org_members_quota is not None and org_members >= org_members_quota: + error_msg = 'Failed. You can only invite %d members.' % org_members_quota + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # create user + try: + user = User.objects.create_user(email, + password, is_staff=False, is_active=True) + except User.DoesNotExist as e: + logger.error(e) + error_msg = 'Fail to add user %s.' % email + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + # add user to org + # set `is_staff` parameter as `0` + try: + ccnet_api.add_org_user(org_id, email, 0) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + name = request.POST.get('name', None) + if name: + Profile.objects.add_or_update(email, name) + + if config.FORCE_PASSWORD_CHANGE: + UserOptions.objects.set_force_passwd_change(email) + + user_info = get_org_user_info(org_id, user) + return Response(user_info) + + +class AdminOrgUser(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser, IsProVersion) + + def put(self, request, org_id, email): + """ update active of a org user + + Permission checking: + 1. only admin can perform this action. + """ + + # argument check + org_id = int(org_id) + if org_id == 0: + error_msg = 'org_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + org = ccnet_api.get_org_by_id(org_id) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not org: + error_msg = 'Organization %d not found.' % org_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + error_msg = 'User %s not found.' % email + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + active = request.data.get('active', None) + if not active: + error_msg = 'active invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if active.lower() not in ('true', 'false'): + error_msg = "active invalid, should be 'true' or 'false'." + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + active_user = True if active.lower() == 'true' else False + + try: + if active_user: + user.is_active = True + else: + user.is_active = False + + # update user status + result_code = user.save() + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if result_code == -1: + error_msg = 'Fail to add user %s.' % email + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # clear web api and repo sync token + # when inactive an user + try: + if not active_user: + clear_token(email) + except Exception as e: + logger.error(e) + + user_info = get_org_user_info(org_id, user) + return Response(user_info) + + def delete(self, request, org_id, email): + """ Delete an user from org + + Permission checking: + 1. only admin can perform this action. + """ + # argument check + org_id = int(org_id) + if org_id == 0: + error_msg = 'org_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + org = ccnet_api.get_org_by_id(org_id) + if not org: + error_msg = 'Organization %d not found.' % org_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + error_msg = 'User %s not found.' % email + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if org.creator == email: + error_msg = 'Failed to delete: %s is an organization creator.' % email + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + ccnet_api.remove_org_user(org_id, email) + user.delete() + 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/api2/permissions.py b/seahub/api2/permissions.py index a3643918e3..76ab915984 100644 --- a/seahub/api2/permissions.py +++ b/seahub/api2/permissions.py @@ -8,6 +8,7 @@ from rest_framework.permissions import BasePermission from django.conf import settings from seaserv import check_permission, is_repo_owner, ccnet_api +from seahub.utils import is_pro_version from seahub.utils import is_pro_version @@ -67,12 +68,14 @@ class CanInviteGuest(BasePermission): return settings.ENABLE_GUEST_INVITATION and \ request.user.permissions.can_invite_guest() + class CanGenerateShareLink(BasePermission): """Check user has permission to generate share link. """ def has_permission(self, request, *args, **kwargs): return request.user.permissions.can_generate_share_link() + class CanGenerateUploadLink(BasePermission): """Check user has permission to generate upload link. """ diff --git a/seahub/base/accounts.py b/seahub/base/accounts.py index f778366e31..a9d2ce0764 100644 --- a/seahub/base/accounts.py +++ b/seahub/base/accounts.py @@ -16,11 +16,10 @@ from constance import config from registration import signals from seahub.auth import login -from seahub.constants import DEFAULT_USER from seahub.profile.models import Profile, DetailedProfile from seahub.role_permissions.utils import get_enabled_role_permissions_by_role from seahub.utils import is_user_password_strong, \ - clear_token, get_system_admins + clear_token, get_system_admins, is_pro_version from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY from seahub.utils.licenseparse import user_number_over_limit @@ -215,7 +214,10 @@ class User(object): source = "LDAP" username = self.username - orgs = ccnet_threaded_rpc.get_orgs_by_user(username) + + orgs = [] + if is_pro_version(): + orgs = ccnet_threaded_rpc.get_orgs_by_user(username) # remove owned repos owned_repos = [] diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 2685f0ab51..3df80bced6 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -15,7 +15,8 @@ from constance import config from seahub.settings import SEAFILE_VERSION, SITE_TITLE, SITE_NAME, \ MAX_FILE_NAME, BRANDING_CSS, LOGO_PATH, LOGO_WIDTH, LOGO_HEIGHT,\ - SHOW_REPO_DOWNLOAD_BUTTON, SITE_ROOT, ENABLE_GUEST_INVITATION + SHOW_REPO_DOWNLOAD_BUTTON, SITE_ROOT, ENABLE_GUEST_INVITATION, \ + FAVICON_PATH try: from seahub.settings import SEACLOUD_MODE @@ -61,6 +62,7 @@ def base(request): 'seafile_version': SEAFILE_VERSION, 'site_title': SITE_TITLE, 'branding_css': BRANDING_CSS, + 'favicon_path': FAVICON_PATH, 'logo_path': LOGO_PATH, 'logo_width': LOGO_WIDTH, 'logo_height': LOGO_HEIGHT, diff --git a/seahub/group/templates/group/add_member_email.html b/seahub/group/templates/group/add_member_email.html index 88932fca60..f5890e7db8 100644 --- a/seahub/group/templates/group/add_member_email.html +++ b/seahub/group/templates/group/add_member_email.html @@ -8,7 +8,7 @@

{% trans "Hi, " %}

-{% blocktrans with grp_name=group.group_name %}{{ email }} invited you to join group {{ grp_name }} on {{ site_name }}: {% endblocktrans %}
+{% blocktrans with grp_name=group.group_name %}{{ email|email2nickname|escape }} invited you to join group {{ grp_name }} on {{ site_name }}: {% endblocktrans %}
{{ url_base }}{% url 'registration_register' %}?src={{ to_email }}

diff --git a/seahub/notifications/management/commands/send_notices.py b/seahub/notifications/management/commands/send_notices.py index e97b74fa54..ccef67efbc 100644 --- a/seahub/notifications/management/commands/send_notices.py +++ b/seahub/notifications/management/commands/send_notices.py @@ -137,7 +137,7 @@ class Command(BaseCommand): notice.grpjoin_user_profile_url = reverse('user_profile', args=[username]) notice.grpjoin_group_url = reverse('group_members', args=[group_id]) - notice.notice_from = username + notice.notice_from = escape(email2nickname(username)) notice.grpjoin_group_name = group.group_name notice.grpjoin_request_msg = join_request_msg notice.avatar_src = self.get_avatar_src(username) @@ -150,7 +150,7 @@ class Command(BaseCommand): group = ccnet_api.get_group(group_id) - notice.notice_from = group_staff + notice.notice_from = escape(email2nickname(group_staff)) notice.avatar_src = self.get_avatar_src(group_staff) notice.group_staff_profile_url = reverse('user_profile', args=[group_staff]) diff --git a/seahub/profile/templates/profile/set_profile.html b/seahub/profile/templates/profile/set_profile.html index 72993ed0e4..2b898f94ba 100644 --- a/seahub/profile/templates/profile/set_profile.html +++ b/seahub/profile/templates/profile/set_profile.html @@ -18,7 +18,7 @@ {% if two_factor_auth_enabled %}
  • {% trans "Two-Factor Authentication" %}
  • {% endif %} -
  • {% trans "Delete Account" %}
  • +
  • {% trans "Delete Account" %}
  • @@ -38,7 +38,7 @@
    {% csrf_token %} {% if not is_ldap_user %} - {% trans "Update" %}
    +
    {% trans "Update" %}
    {% endif %} {% for error in form.nickname.errors %} diff --git a/seahub/settings.py b/seahub/settings.py index 71edd5bc84..9afb83e346 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -393,6 +393,10 @@ SITE_TITLE = 'Private Seafile' # Base name used in email sending SITE_NAME = 'Seafile' +# Path to the favicon file (relative to the media path) +# tip: use a different name when modify it. +FAVICON_PATH = 'img/favicon.ico' + # Path to the Logo Imagefile (relative to the media path) LOGO_PATH = 'img/seafile-logo.png' # logo size. the unit is 'px' @@ -563,6 +567,9 @@ CLOUD_DEMO_USER = 'demo@seafile.com' ENABLE_TWO_FACTOR_AUTH = False OTP_LOGIN_URL = '/profile/two_factor_authentication/setup/' +# Enable personal wiki, group wiki +ENABLE_WIKI = True + ##################### # External settings # ##################### diff --git a/seahub/templates/base.html b/seahub/templates/base.html index 16c66e10c8..23c28318be 100644 --- a/seahub/templates/base.html +++ b/seahub/templates/base.html @@ -10,10 +10,7 @@ {% block viewport %} {% endblock %} - - + @@ -39,7 +36,7 @@ {% if seacloud_mode %} logo {% else %} - logo + logo {% endif %} diff --git a/seahub/templates/base_for_backbone.html b/seahub/templates/base_for_backbone.html index 1a80309016..abb69f992d 100644 --- a/seahub/templates/base_for_backbone.html +++ b/seahub/templates/base_for_backbone.html @@ -9,11 +9,7 @@ - - - + {% compress css %} @@ -47,7 +43,7 @@ {% if seacloud_mode %} logo {% else %} - logo + logo {% endif %} diff --git a/seahub/templates/file_revisions.html b/seahub/templates/file_revisions.html index b9cbcd58f2..18bddc9108 100644 --- a/seahub/templates/file_revisions.html +++ b/seahub/templates/file_revisions.html @@ -111,8 +111,9 @@ $('.restore-file').click(function() { dataType: 'json', beforeSend: prepareCSRFToken, success: function() { - _this.closest('tr').remove(); - feedback("{% trans "Success" %}", 'success'); + var msg = "{% trans "Successfully restored {filename}" %}".replace('{filename}', "{{u_filename|escapejs}}"); + feedback(msg, 'success'); + setTimeout(function() { location.reload(true); }, 1000); }, error: ajaxErrorHandler }); diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index a2dc16810f..62f7d0f9f1 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -659,7 +659,7 @@ {% endblock %} diff --git a/seahub/templates/sysadmin/sysadmin_backbone.html b/seahub/templates/sysadmin/sysadmin_backbone.html index 50ae39a63d..851d9c360b 100644 --- a/seahub/templates/sysadmin/sysadmin_backbone.html +++ b/seahub/templates/sysadmin/sysadmin_backbone.html @@ -8,10 +8,8 @@ - - + + {% compress css %} @@ -41,7 +39,7 @@ {% if seacloud_mode %} logo {% else %} - logo + logo {% endif %} diff --git a/seahub/templates/sysadmin/user_add_email.html b/seahub/templates/sysadmin/user_add_email.html index 7b6690c51c..f6b85ad0ea 100644 --- a/seahub/templates/sysadmin/user_add_email.html +++ b/seahub/templates/sysadmin/user_add_email.html @@ -10,9 +10,9 @@

    {% if org %} -{% blocktrans with org_name=org.org_name %}{{ user }} invited you to join organization "{{ org_name }}" on {{ site_name }}.{% endblocktrans%} +{% blocktrans with org_name=org.org_name %}{{ user|email2nickname|escape }} invited you to join organization "{{ org_name }}" on {{ site_name }}.{% endblocktrans%} {% else %} -{% blocktrans %}{{ user }} invited you to join {{ site_name }}.{% endblocktrans%} +{% blocktrans %}{{ user|email2nickname|escape }} invited you to join {{ site_name }}.{% endblocktrans%} {% endif %}

    diff --git a/seahub/templates/sysadmin/user_batch_add_email.html b/seahub/templates/sysadmin/user_batch_add_email.html index 97a237dd7f..fa55ec1816 100644 --- a/seahub/templates/sysadmin/user_batch_add_email.html +++ b/seahub/templates/sysadmin/user_batch_add_email.html @@ -9,7 +9,7 @@

    {% trans "Hi," %}

    -{% blocktrans %}{{ user }} invited you to join {{ site_name }}.{% endblocktrans%} +{% blocktrans %}{{ user|email2nickname|escape }} invited you to join {{ site_name }}.{% endblocktrans%}

    diff --git a/seahub/urls.py b/seahub/urls.py index 7e9524bc09..5dc4216ea1 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -49,6 +49,7 @@ from seahub.api2.endpoints.admin.device_errors import AdminDeviceErrors 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.default_library import AdminDefaultLibrary from seahub.api2.endpoints.admin.trash_libraries import AdminTrashLibraries, AdminTrashLibrary from seahub.api2.endpoints.admin.groups import AdminGroups, AdminGroup from seahub.api2.endpoints.admin.group_libraries import AdminGroupLibraries, AdminGroupLibrary @@ -56,6 +57,7 @@ from seahub.api2.endpoints.admin.group_members import AdminGroupMembers, AdminGr from seahub.api2.endpoints.admin.shares import AdminShares from seahub.api2.endpoints.admin.users_batch import AdminUsersBatch from seahub.api2.endpoints.admin.logs import AdminLogs +from seahub.api2.endpoints.admin.org_users import AdminOrgUsers, AdminOrgUser # Uncomment the next two lines to enable the admin: #from django.contrib import admin @@ -211,6 +213,7 @@ urlpatterns = patterns( url(r'^api/v2.1/admin/groups/(?P\d+)/members/(?P[^/]+)/$', AdminGroupMember.as_view(), name='api-v2.1-admin-group-member'), 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/default-library/$', AdminDefaultLibrary.as_view(), name='api-v2.1-admin-default-library'), url(r'^api/v2.1/admin/trash-libraries/$', AdminTrashLibraries.as_view(), name='api-v2.1-admin-trash-libraries'), url(r'^api/v2.1/admin/trash-libraries/(?P[-0-9a-f]{36})/$', AdminTrashLibrary.as_view(), name='api-v2.1-admin-trash-library'), url(r'^api/v2.1/admin/shares/$', AdminShares.as_view(), name='api-v2.1-admin-shares'), @@ -218,6 +221,9 @@ urlpatterns = patterns( url(r'^api/v2.1/admin/users/batch/$', AdminUsersBatch.as_view(), name='api-v2.1-admin-users-batch'), + url(r'^api/v2.1/admin/organizations/(?P\d+)/users/$', AdminOrgUsers.as_view(), name='api-v2.1-admin-org-users'), + url(r'^api/v2.1/admin/organizations/(?P\d+)/users/(?P[^/]+)/$', AdminOrgUser.as_view(), name='api-v2.1-admin-org-user'), + (r'^avatar/', include('seahub.avatar.urls')), (r'^notification/', include('seahub.notifications.urls')), (r'^contacts/', include('seahub.contacts.urls')), diff --git a/seahub/utils/licenseparse.py b/seahub/utils/licenseparse.py index 8faabceecc..c120ec216b 100644 --- a/seahub/utils/licenseparse.py +++ b/seahub/utils/licenseparse.py @@ -48,6 +48,7 @@ def parse_license(): return ret def user_number_over_limit(new_users = 0): + logger = logging.getLogger(__name__) if is_pro_version(): try: # get license user limit @@ -60,11 +61,16 @@ def user_number_over_limit(new_users = 0): active_users = active_db_users + active_ldap_users if \ active_ldap_users > 0 else active_db_users - return active_users + new_users >= max_users + if new_users < 0: + logger.debug('`new_users` must be greater or equal to 0.') + return False + elif new_users == 0: + return active_users >= max_users + else: + return active_users + new_users > max_users + except Exception as e: - logger = logging.getLogger(__name__) logger.error(e) return False else: return False - diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 261136e550..f771bfc52f 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -748,6 +748,7 @@ def libraries(request): "allow_public_share": allow_public_share, "guide_enabled": guide_enabled, "sub_lib_enabled": sub_lib_enabled, + 'enable_wiki': settings.ENABLE_WIKI, 'enable_upload_folder': settings.ENABLE_UPLOAD_FOLDER, 'enable_resumable_fileupload': settings.ENABLE_RESUMABLE_FILEUPLOAD, 'max_number_of_files_for_fileupload': settings.MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, diff --git a/seahub/views/file.py b/seahub/views/file.py index 2798c74bf3..bf62bdc1f7 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -1282,9 +1282,10 @@ def download_file(request, repo_id, obj_id): if repo.encrypted and not seafile_api.is_password_set(repo_id, username): return HttpResponseRedirect(reverse('view_common_lib_dir', args=[repo_id, ''])) - # Permission check and generate download link - path = request.GET.get('p', '') - if check_folder_permission(request, repo_id, path): + # only check the permissions at the repo level + # to prevent file can not be downloaded on the history page + # if it has been renamed + if check_folder_permission(request, repo_id, '/'): # Get a token to access file token = seafile_api.get_fileserver_access_token(repo_id, obj_id, 'download', username) @@ -1293,11 +1294,11 @@ def download_file(request, repo_id, obj_id): next = request.META.get('HTTP_REFERER', settings.SITE_ROOT) return HttpResponseRedirect(next) - # send stats message - send_file_access_msg(request, repo, path, 'web') - + path = request.GET.get('p', '') + send_file_access_msg(request, repo, path, 'web') # send stats message file_name = os.path.basename(path.rstrip('/')) - redirect_url = gen_file_get_url(token, file_name) + redirect_url = gen_file_get_url(token, file_name) # generate download link + return HttpResponseRedirect(redirect_url) ########## text diff diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index d65d66655a..f746dc0070 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -1811,7 +1811,7 @@ def batch_add_user(request): filestream = StringIO.StringIO(content) reader = csv.reader(filestream) new_users_count = len(list(reader)) - if user_number_over_limit(new_users = new_users_count): + if user_number_over_limit(new_users=new_users_count): messages.error(request, _(u'The number of users exceeds the limit.')) return HttpResponseRedirect(next) @@ -1823,47 +1823,48 @@ def batch_add_user(request): if not row: continue - username = row[0].strip() or '' - if not is_valid_username(username): - continue - - password = row[1].strip() or '' - if password == '': + try: + username = row[0].strip() + password = row[1].strip() + if not is_valid_username(username) or not password: + continue + except Exception as e: + logger.error(e) continue try: User.objects.get(email=username) except User.DoesNotExist: - User.objects.create_user(username, - password, is_staff=False, is_active=True) + User.objects.create_user( + username, password, is_staff=False, is_active=True) if config.FORCE_PASSWORD_CHANGE: UserOptions.objects.set_force_passwd_change(username) # then update the user's optional info try: - nickname = row[2].strip() or '' + nickname = row[2].strip() if len(nickname) <= 64 and '/' not in nickname: Profile.objects.add_or_update(username, nickname, '') except Exception as e: logger.error(e) try: - department = row[3].strip() or '' + department = row[3].strip() if len(department) <= 512: DetailedProfile.objects.add_or_update(username, department, '') except Exception as e: logger.error(e) try: - role = row[4].strip() or '' + role = row[4].strip() if is_pro_version() and role in get_available_roles(): User.objects.update_role(username, role) except Exception as e: logger.error(e) try: - space_quota_mb = row[5].strip() or '' + space_quota_mb = row[5].strip() space_quota_mb = int(space_quota_mb) if space_quota_mb >= 0: space_quota = int(space_quota_mb) * get_file_size_unit('MB') @@ -1885,7 +1886,7 @@ def batch_add_user(request): "email": username, } admin_operation.send(sender=None, admin_name=request.user.username, - operation=USER_ADD, detail=admin_op_detail) + operation=USER_ADD, detail=admin_op_detail) messages.success(request, _('Import succeeded')) else: diff --git a/static/scripts/app/views/dialogs/dirent-mvcp.js b/static/scripts/app/views/dialogs/dirent-mvcp.js index dcb4ca1a28..a9c670d9d3 100644 --- a/static/scripts/app/views/dialogs/dirent-mvcp.js +++ b/static/scripts/app/views/dialogs/dirent-mvcp.js @@ -17,6 +17,9 @@ define([ this.dir = options.dir; this.op_type = options.op_type; + this.dirView = options.dirView; + this.imgIndex = options.imgIndex; + // only show current repo if current repo is read-write this.show_cur_repo = this.dirent.get('perm') == 'rw' ? true : false; @@ -64,6 +67,15 @@ define([ return this; }, + removeImgItem: function() { + if (this.dirent.get('is_img')) { + this.dirView.updateMagnificPopupOptions({ + 'op': 'delete-item', + 'index': this.imgIndex + }); + } + }, + events: { 'submit form': 'formSubmit' }, @@ -112,6 +124,7 @@ define([ $.modal.close(); if (op == 'mv') { _this.dir.remove(_this.dirent); + _this.removeImgItem(); } Common.feedback(msg, 'success'); } else { // failed or canceled @@ -208,6 +221,7 @@ define([ if (!data['task_id']) { // no progress if (_this.op_type == 'mv') { _this.dir.remove(_this.dirent); + _this.removeImgItem(); } Common.feedback(msg, 'success'); } else { diff --git a/static/scripts/app/views/dialogs/dirent-rename.js b/static/scripts/app/views/dialogs/dirent-rename.js index 364dd1829c..f369adce33 100644 --- a/static/scripts/app/views/dialogs/dirent-rename.js +++ b/static/scripts/app/views/dialogs/dirent-rename.js @@ -14,6 +14,9 @@ define([ this.dirent = options.dirent; this.dir = options.dir; + this.dirView = options.dirView; + this.imgIndex = options.imgIndex; + this.render(); this.$el.modal({appendTo:'#main', focus: false}); $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); @@ -65,6 +68,14 @@ define([ newname: new_name, success: function() { $.modal.close(); + + if (_this.dirent.get('is_img')) { + _this.dirView.updateMagnificPopupOptions({ + 'op': 'update-item', + 'index': _this.imgIndex, + 'model': _this.dirent + }); + } }, error: function(xhr, textStatus, errorThrown) { var err; diff --git a/static/scripts/app/views/dir.js b/static/scripts/app/views/dir.js index 811cbf57ae..13e2ee71b4 100644 --- a/static/scripts/app/views/dir.js +++ b/static/scripts/app/views/dir.js @@ -219,22 +219,61 @@ define([ } }, - updateMagnificPopupOptions: function() { - var imgs = this.dir.where({is_img: true}); - var items = []; + updateMagnificPopupOptions: function(options) { var repo_id = this.dir.repo_id, path = this.dir.path; - $(imgs).each(function(index, model) { + var genItem = function(model) { var name = model.get('obj_name'); var dirent_path = Common.pathJoin([path, name]); - items.push({ + var item = { 'name': name, 'url': model.getWebUrl(), 'src': app.config.siteRoot + 'repo/' + repo_id + '/raw' + Common.encodePath(dirent_path) - }); - }); + }; + return item; + }; - this.magnificPopupOptions.items = items; + var _this = this; + var getItems = function() { + var imgs = _this.dir.where({is_img: true}); + var items = []; + $(imgs).each(function(index, model) { + var item = genItem(model); + items.push(item); + }); + _this.magnificPopupOptions.items = items; + }; + + var addNewItem = function(model) { + var item = genItem(model); + // add the new item as the first + _this.magnificPopupOptions.items.unshift(item); + }; + + var updateItem = function(index, model) { + var item = genItem(model); + _this.magnificPopupOptions.items[index] = item; + }; + + var deleteItem = function(index) { + _this.magnificPopupOptions.items.splice(index, 1); + }; + + var op = options ? options.op : 'get-items'; + switch (op) { + case 'get-items': + getItems(); + break; + case 'add-new-item': + addNewItem(options.model); + break; + case 'update-item': + updateItem(options.index, options.model); + break; + case 'delete-item': + deleteItem(options.index); + break; + } }, // for fileupload @@ -712,6 +751,10 @@ define([ } } } + + if (new_dirent.get('is_img')) { + this.updateMagnificPopupOptions({'op':'add-new-item', 'model':new_dirent}); + } }, addNewDir: function(new_dirent) { @@ -910,7 +953,7 @@ define([ data: { 'dirents_names': selected_names }, - success: function(data) { + success: function(data) { // data['deleted']: [name,] var del_len = data['deleted'].length, not_del_len = data['undeleted'].length, msg_s, msg_f; @@ -937,6 +980,8 @@ define([ } msg_s = msg_s.replace('%(name)s', data['deleted'][0]).replace('%(amount)s', del_len - 1); Common.feedback(msg_s, 'success'); + + _this.updateMagnificPopupOptions(); // after Successfully deleting some items } if (not_del_len > 0) { if (not_del_len == 1) { @@ -1082,6 +1127,7 @@ define([ } else { msg_s = gettext("Successfully moved %(name)s and %(amount)s other items."); } + _this.updateMagnificPopupOptions(); // after moving items in the same library } else { // cp if (success_len == 1) { msg_s = gettext("Successfully copied %(name)s."); @@ -1230,12 +1276,18 @@ define([ var endOrContinue = function () { if (i == op_objs.length - 1) { setTimeout(function () { $.modal.close(); }, 500); + if (op == 'mv') { + _this.updateMagnificPopupOptions(); + } } else { mvcpDirent(++i); } }; var end = function () { setTimeout(function () { $.modal.close(); }, 500); + if (op == 'mv') { + _this.updateMagnificPopupOptions(); + } }; mvcpDirent(); cancel_btn.click(function() { diff --git a/static/scripts/app/views/dirent-grid.js b/static/scripts/app/views/dirent-grid.js index 06a071e9ba..01bf61af09 100644 --- a/static/scripts/app/views/dirent-grid.js +++ b/static/scripts/app/views/dirent-grid.js @@ -64,6 +64,7 @@ define([ if (this.model.get('is_img')) { // use specific links such as .img-link, .text-link, in order to make 'open by index' work this.$('.img-link, .text-link').magnificPopup(this.dirView.magnificPopupOptions); + this.$el.addClass('img-grid-item'); } return this; @@ -107,6 +108,7 @@ define([ var op = template({ dirent: this.model.attributes, dirent_path: this.model.getPath(), + download_url: this.model.getDownloadUrl(), category: dir.category, repo_id: dir.repo_id, is_repo_owner: dir.is_repo_owner, @@ -126,7 +128,7 @@ define([ // Using _.bind(function, object) to make that whenever the function is // called, the value of this will be the object. - this.$('.download').on('click', _.bind(this.download, this)); + this.$('.download-dir').on('click', _.bind(this.download, this)); this.$('.delete').on('click', _.bind(this.del, this)); this.$('.share').on('click', _.bind(this.share, this)); this.$('.mv').on('click', _.bind(this.mvcp, this)); @@ -142,7 +144,7 @@ define([ viewImageWithPopup: function() { if (this.model.get('is_img')) { - var index = _.indexOf(this.dir.where({'is_img': true}), this.model); + var index = $('.img-grid-item', this.dirView.$dirent_grid).index(this.$el); $.magnificPopup.open(this.dirView.magnificPopupOptions, index); // open by index } }, @@ -159,7 +161,12 @@ define([ return false; }, - del: function(event) { + del: function() { + var _this = this; + if (this.model.get('is_img')) { + var index = $('.img-grid-item', this.dirView.$dirent_grid).index(this.$el); + } + this.closeMenu(); var dirent_name = this.model.get('obj_name'); this.model.deleteFromServer({ @@ -167,6 +174,10 @@ define([ var msg = gettext("Successfully deleted %(name)s") .replace('%(name)s', dirent_name); Common.feedback(msg, 'success'); + + if (_this.model.get('is_img')) { + _this.dirView.updateMagnificPopupOptions({'op':'delete-item', 'index':index}); + } }, error: function(xhr) { Common.ajaxErrorHandler(xhr); @@ -203,6 +214,14 @@ define([ 'op_type': op_type }; + if (this.model.get('is_img') && op_type == 'mv') { + var index = $('.img-grid-item', this.dirView.$dirent_grid).index(this.$el); + $.extend(options, { + 'dirView': this.dirView, + 'imgIndex': index + }); + } + new DirentMvcpDialog(options); this.closeMenu(); return false; @@ -214,6 +233,14 @@ define([ 'dir': this.dir, 'dirent': this.model }; + + if (this.model.get('is_img')) { + var index = $('.img-grid-item', this.dirView.$dirent_grid).index(this.$el); + $.extend(options, { + 'dirView': this.dirView, + 'imgIndex': index + }); + } new DirentRenameDialog(options); return false; }, diff --git a/static/scripts/app/views/dirent.js b/static/scripts/app/views/dirent.js index 8f3e3dd921..225eefc5bf 100644 --- a/static/scripts/app/views/dirent.js +++ b/static/scripts/app/views/dirent.js @@ -325,12 +325,21 @@ define([ }, del: function() { + var _this = this; + if (this.model.get('is_img')) { + var index = $('.img-name-link', this.dirView.$dirent_list).index(this.$('.img-name-link')); + } + var dirent_name = this.model.get('obj_name'); this.model.deleteFromServer({ success: function(data) { var msg = gettext("Successfully deleted %(name)s") .replace('%(name)s', dirent_name); Common.feedback(msg, 'success'); + + if (_this.model.get('is_img')) { + _this.dirView.updateMagnificPopupOptions({'op':'delete-item', 'index':index}); + } }, error: function(xhr) { Common.ajaxErrorHandler(xhr); @@ -340,6 +349,7 @@ define([ }, rename: function() { + var _this = this; var dirent_name = this.model.get('obj_name'); var form = $(this.renameTemplate({ @@ -372,6 +382,15 @@ define([ if (app.ui.currentHighlightedItem) { app.ui.currentHighlightedItem.rmHighlight(); } + + if (_this.model.get('is_img')) { + var index = $('.img-name-link', _this.dirView.$dirent_list).index(_this.$('.img-name-link')); + _this.dirView.updateMagnificPopupOptions({ + 'op': 'update-item', + 'index': index, + 'model': _this.model + }); // update the item + } }; var cancelRename = function() { app.ui.freezeItemHightlight = false; @@ -429,6 +448,13 @@ define([ 'dirent': this.model, 'op_type': op_type }; + if (this.model.get('is_img') && op_type == 'mv') { + var index = $('.img-name-link', this.dirView.$dirent_list).index(this.$('.img-name-link')); + $.extend(options, { + 'dirView': this.dirView, + 'imgIndex': index + }); + } this._hideMenu(); new DirentMvcpDialog(options); diff --git a/static/scripts/app/views/fileupload.js b/static/scripts/app/views/fileupload.js index f3c1983e90..55aeceb570 100644 --- a/static/scripts/app/views/fileupload.js +++ b/static/scripts/app/views/fileupload.js @@ -268,7 +268,7 @@ define([ } } }); - $('#simplemodal-container').css({'height':'auto'}); + $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); $('.yes', confirm_popup).click(function() { var selected_file = dirents.findWhere({'obj_name': file.name}); if (selected_file.get('is_locked')) { diff --git a/tests/api/endpoints/admin/test_default_library.py b/tests/api/endpoints/admin/test_default_library.py new file mode 100644 index 0000000000..fd4caef6c6 --- /dev/null +++ b/tests/api/endpoints/admin/test_default_library.py @@ -0,0 +1,57 @@ +import json + +from django.core.urlresolvers import reverse +from seahub.options.models import UserOptions +from seahub.test_utils import BaseTestCase + +class DefaultLibraryTest(BaseTestCase): + + def setUp(self): + self.user_name = self.user.username + self.admin_name = self.admin.username + self.group_id = self.group.id + self.repo_id = self.repo.id + + self.endpoint = reverse('api-v2.1-admin-default-library') + + def tearDown(self): + self.remove_group() + + def test_can_get(self): + self.login_as(self.admin) + + assert UserOptions.objects.get_default_repo(self.user_name) is None + + resp = self.client.get(self.endpoint + '?user_email=%s' % self.user_name) + json_resp = json.loads(resp.content) + + assert json_resp['user_email'] == self.user_name + assert json_resp['exists'] == False + + def test_can_create(self): + self.login_as(self.admin) + + assert UserOptions.objects.get_default_repo(self.user_name) is None + + data = {'user_email': self.user_name} + resp = self.client.post(self.endpoint, data) + json_resp = json.loads(resp.content) + + new_default_repo_id = UserOptions.objects.get_default_repo(self.user_name) + + assert json_resp['user_email'] == self.user_name + assert json_resp['exists'] == True + assert json_resp['repo_id'] == new_default_repo_id + + def test_can_not_get_if_not_admin(self): + self.login_as(self.user) + + resp = self.client.get(self.endpoint + '?user_email=%s' % self.user_name) + self.assertEqual(403, resp.status_code) + + def test_can_not_create_if_not_admin(self): + self.login_as(self.user) + + data = {'user_email': self.user_name} + resp = self.client.post(self.endpoint, data) + self.assertEqual(403, resp.status_code) diff --git a/tests/api/endpoints/admin/test_org_users.py b/tests/api/endpoints/admin/test_org_users.py new file mode 100644 index 0000000000..cecefc2278 --- /dev/null +++ b/tests/api/endpoints/admin/test_org_users.py @@ -0,0 +1,248 @@ +import json +from mock import patch + +from seaserv import ccnet_api +from django.core.urlresolvers import reverse +from seahub.test_utils import BaseTestCase +from tests.common.utils import randstring + +from seaserv import seafserv_threaded_rpc + +try: + from seahub.settings import LOCAL_PRO_DEV_ENV +except ImportError: + LOCAL_PRO_DEV_ENV = False + +def remove_org(org_id): + org_id = int(org_id) + org = ccnet_api.get_org_by_id(org_id) + if org: + users =ccnet_api.get_org_emailusers(org.url_prefix, -1, -1) + for u in users: + ccnet_api.remove_org_user(org_id, u.email) + + groups = ccnet_api.get_org_groups(org.org_id, -1, -1) + for g in groups: + ccnet_api.remove_org_group(org_id, g.gid) + + # remove org repos + seafserv_threaded_rpc.remove_org_repo_by_org_id(org_id) + + # remove org + ccnet_api.remove_org(org_id) + +class OrgUsersTest(BaseTestCase): + + def setUp(self): + + self.user_name = self.user.username + self.admin_name = self.admin.username + + if LOCAL_PRO_DEV_ENV: + self.org_name = randstring(6) + self.org_url_prefix = randstring(6) + tmp_user = self.create_user(email='%s@%s.com' % (randstring(6), randstring(6))) + self.org_creator = tmp_user.username + + self.org_id = ccnet_api.create_org(self.org_name, + self.org_url_prefix, self.org_creator) + self.org_users_url = reverse('api-v2.1-admin-org-users', + args=[self.org_id]) + + def tearDown(self): + self.remove_group() + self.remove_repo() + + if LOCAL_PRO_DEV_ENV: + remove_org(self.org_id) + self.remove_user(self.org_creator) + + def test_can_create(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.admin) + + email = '%s@%s.com' % (randstring(6), randstring(6)) + data = {'email': email, 'password': randstring(6)} + resp = self.client.post(self.org_users_url, data) + json_resp = json.loads(resp.content) + self.assertEqual(200, resp.status_code) + + assert json_resp['email'] == email + + def test_can_not_create_if_not_admin(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.user) + + email = '%s@%s.com' % (randstring(6), randstring(6)) + data = {'email': email, 'password': randstring(6)} + resp = self.client.post(self.org_users_url, data) + self.assertEqual(403, resp.status_code) + + def test_create_with_invalid_org_id(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.admin) + + invalid_org_users_url = reverse('api-v2.1-admin-org-users', args=[0]) + + email = '%s@%s.com' % (randstring(6), randstring(6)) + data = {'email': email, 'password': randstring(6)} + resp = self.client.post(invalid_org_users_url, data) + self.assertEqual(400, resp.status_code) + + def test_create_with_existed_user(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.admin) + + data = {'email': self.admin_name, 'password': randstring(6)} + resp = self.client.post(self.org_users_url, data) + self.assertEqual(400, resp.status_code) + + @patch('seahub.api2.endpoints.admin.org_users.user_number_over_limit') + def test_create_with_user_number_over_limit(self, mock_user_number_over_limit): + + if not LOCAL_PRO_DEV_ENV: + return + + mock_user_number_over_limit.return_value = True + + self.login_as(self.admin) + + email = '%s@%s.com' % (randstring(6), randstring(6)) + data = {'email': email, 'password': randstring(6)} + resp = self.client.post(self.org_users_url, data) + self.assertEqual(403, resp.status_code) + +class OrgUserTest(BaseTestCase): + + def setUp(self): + self.user_name = self.user.username + self.admin_name = self.admin.username + + if LOCAL_PRO_DEV_ENV: + self.org_name = randstring(6) + self.org_url_prefix = randstring(6) + tmp_user = self.create_user(email='%s@%s.com' % (randstring(6), randstring(6))) + self.org_creator = tmp_user.username + + self.org_id = ccnet_api.create_org(self.org_name, + self.org_url_prefix, self.org_creator) + self.org_users_url = reverse('api-v2.1-admin-org-users', + args=[self.org_id]) + + def tearDown(self): + self.remove_group() + self.remove_repo() + + if LOCAL_PRO_DEV_ENV: + remove_org(self.org_id) + self.remove_user(self.org_creator) + + def test_can_delete(self): + + if not LOCAL_PRO_DEV_ENV: + return + + email = '%s@%s.com' % (randstring(6), randstring(6)) + self.create_user(email=email) + ccnet_api.add_org_user(self.org_id, email, 0) + assert ccnet_api.org_user_exists(self.org_id, email) == 1 + + self.login_as(self.admin) + url = reverse('api-v2.1-admin-org-user', args=[self.org_id, email]) + resp = self.client.delete(url) + + self.assertEqual(200, resp.status_code) + assert ccnet_api.org_user_exists(self.org_id, email) == 0 + + def test_can_not_delete_if_not_admin(self): + + if not LOCAL_PRO_DEV_ENV: + return + + email = '%s@%s.com' % (randstring(6), randstring(6)) + self.create_user(email=email) + ccnet_api.add_org_user(self.org_id, email, 0) + assert ccnet_api.org_user_exists(self.org_id, email) == 1 + + self.login_as(self.user) + url = reverse('api-v2.1-admin-org-user', args=[self.org_id, email]) + resp = self.client.delete(url) + + self.assertEqual(403, resp.status_code) + + def test_delete_org_creator(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.admin) + url = reverse('api-v2.1-admin-org-user', args=[self.org_id, + self.org_creator]) + resp = self.client.delete(url) + + self.assertEqual(403, resp.status_code) + + def test_delete_invalid_user(self): + + if not LOCAL_PRO_DEV_ENV: + return + + not_existed_user = '%s@%s.com' % (randstring(6), randstring(6)) + + self.login_as(self.admin) + url = reverse('api-v2.1-admin-org-user', args=[self.org_id, + not_existed_user]) + resp = self.client.delete(url) + + self.assertEqual(404, resp.status_code) + + def test_can_update(self): + + if not LOCAL_PRO_DEV_ENV: + return + + email = '%s@%s.com' % (randstring(6), randstring(6)) + tmp_user = self.create_user(email=email) + ccnet_api.add_org_user(self.org_id, email, 0) + assert ccnet_api.org_user_exists(self.org_id, email) == 1 + assert tmp_user.is_active + + self.login_as(self.admin) + url = reverse('api-v2.1-admin-org-user', args=[self.org_id, email]) + status = 'false' + data = 'active=%s' % status + resp = self.client.put(url, data, 'application/x-www-form-urlencoded') + + json_resp = json.loads(resp.content) + self.assertEqual(200, resp.status_code) + assert json_resp['active'] is False + + def test_update_with_invalid_args(self): + + if not LOCAL_PRO_DEV_ENV: + return + + email = '%s@%s.com' % (randstring(6), randstring(6)) + tmp_user = self.create_user(email=email) + ccnet_api.add_org_user(self.org_id, email, 0) + assert ccnet_api.org_user_exists(self.org_id, email) == 1 + assert tmp_user.is_active + + self.login_as(self.admin) + url = reverse('api-v2.1-admin-org-user', args=[self.org_id, email]) + status = 'fals' + data = 'active=%s' % status + resp = self.client.put(url, data, 'application/x-www-form-urlencoded') + self.assertEqual(400, resp.status_code)