diff --git a/media/css/seahub.css b/media/css/seahub.css index d238a2ba48..ec8ba519a2 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -128,6 +128,8 @@ .sf2-icon-x1:before { content:"\e01d"; } .sf2-icon-tick:before { content:"\e01e"; } .sf2-icon-x2:before { content:"\e01f"; } +.sf2-icon-user2:before { content:"\e020"; } +.sf2-icon-msgs2:before { content:"\e021"; } /******* tags **********/ html { background:#fff; } @@ -584,30 +586,6 @@ textarea:-moz-placeholder {/* for FF */ top:-11px; left:-10px; } -/**** info-item *****/ -.info-item { - border: 1px solid #eee; - border-radius: 2px; - margin-bottom: 12px; -} -.info-item h3, -.info-item p { - margin:0; -} -.info-item h3 { - font-size:1.15em; -} -.info-item-top { - padding: 4px 10px; - background: #f8f8f8; - border-bottom: 1px solid #eee; -} -.info-item-bottom { - padding: 8px 10px; -} -.info-item p.not-last { - margin-bottom:0.4em; -} /**** tabnav *****/ .tabnav-tabs, .tabnav-tab { @@ -690,6 +668,18 @@ textarea:-moz-placeholder {/* for FF */ color:#333; font-weight:normal; } +.side-tabnav-tabs .tab .grp-list a { + font-size:14px; + line-height:24px; + padding:0; +} +#group-nav .sharp { + display:inline-block; + width:42px; + margin-right:5px; + text-align:right; + color:#aaa; +} .side-tabnav-tabs .tab a:hover { background-color:#feefb8; text-decoration:none; @@ -704,12 +694,16 @@ textarea:-moz-placeholder {/* for FF */ line-height:1; color:#999; } +#group-nav .toggle-icon { + color:#999; +} .side-tabnav-tabs .tab-cur a, .side-tabnav-tabs .tab-cur a:hover { background-color:#feac74; } .side-tabnav-tabs .tab-cur [class^="sf2-icon-"], -.side-tabnav-tabs .tab-cur a { +.side-tabnav-tabs .tab-cur a, +#group-nav .tab-cur .sharp { color:#fff; } .side-tabnav-tabs .tab-cur a { @@ -1146,6 +1140,45 @@ textarea:-moz-placeholder {/* for FF */ color:#fff; text-decoration:none; } +/**** popover ****/ /* e.g. top notice popup, group members popup */ +.popover { + width:240px; + background:#fff; + border:1px solid #c9c9c9; + border-radius:3px; + box-shadow:0 0 1px #f3f3f3; +} +.popover-hd { + padding:5px 0 3px; + border-bottom:1px solid #dfdfe1; + margin:0 10px; +} +.popover-title { + text-align:center; + margin:0; +} +.popover-close { + font-size:16px; + color:#b9b9b9; + margin:4px 0 0; +} +.popover-con { + padding:0 10px; + overflow:auto; +} +/**** user-item ****/ /* e.g. group member in 'group members' panel */ +.user-item { + margin:5px 0; +} +.user-item .avatar { + border-radius:1000px; +} +.user-item .txt { + margin-left:40px; +} +.user-item .txt-item { + margin:0; +} /********** container ***********/ #main, #footer { width:950px; @@ -1209,6 +1242,11 @@ textarea:-moz-placeholder {/* for FF */ #organization-repos .hd { position:relative; } +#group-top .op-icon { + display:inline-block; + font-size:22px; + margin:4px 15px 0 0; +} .commit-list-topbar, .file-audit-list-topbar, .repo-file-list-topbar { @@ -1439,14 +1477,6 @@ textarea:-moz-placeholder {/* for FF */ #user-info-popup { width:220px; } -#top-nav-grp-info { - width:235px; - top:40px; - font-size:14px; -} -#top-nav-grp-info .outer-caret { - left:38px; -} #notice-popup .outer-caret { left:143px; } @@ -1463,35 +1493,10 @@ textarea:-moz-placeholder {/* for FF */ color:#333; font-weight:normal; } -#top-nav-grp-list .item { - padding:5px 10px; - border:none; -} -#top-nav-grp-list .item.hl { - background:#fafafa; - cursor:pointer; -} -#top-nav-grp-list .name { - display:inline-block; - width:142px; -} .top-info-popup a.item:hover { background:#fafafa; text-decoration:none; } -#top-nav-grp-info .avatar { - border-radius:1000px; - margin-right:5px; - vertical-align:middle; -} -#top-nav-grp-info .op-icon { - font-size:24px; - color:#999; - margin:0; -} -#top-nav-grp-info .all-grp { - padding-left:50px; -} #msg-file-share { margin-top:8px; position:relative; @@ -1529,34 +1534,6 @@ textarea:-moz-placeholder {/* for FF */ #logo { margin-right:30px; } -#header .nav { - float:left; - padding-top:8px; - font-size:15px; -} -#header .nav-item { - float: left; - height:21px; - padding-bottom:19px; - margin:0 14px; -} -.ru #header .nav-item { - margin:0 11px; -} -#header .nav .a, -#header .nav .a:visited { - color:#8A948F; - font-weight:bold; - text-decoration:none; - white-space:nowrap;/* for ipad */ -} -#header .nav .a.cur { - color:#000; -} -#header .nav .a:hover { - color: #ff9933; - text-decoration: underline; -} /* footer */ #footer { @@ -1835,6 +1812,7 @@ textarea:-moz-placeholder {/* for FF */ line-height:25px; margin:0; } +#group-top .path, #dir-view .path { font-size:16px; line-height:1.5; @@ -3757,6 +3735,7 @@ img.thumbnail { border-radius:3px; margin:0; } +/**** #groups ****/ /* groups page */ #groups .group-name { font-size:16px; margin:15px 0 10px; @@ -3767,3 +3746,15 @@ img.thumbnail { #groups .empty-tips { margin:10px 0 40px; } +/**** #group ****/ /* group page */ +#group { + position:relative; +} +#group-members { + position:absolute; + top:77px; + right:0; +} +#group-members .outer-caret { + right:66px; +} diff --git a/media/css/sf_font2/seafile-font2.eot b/media/css/sf_font2/seafile-font2.eot index 12bafe7f6c..d6de997a7a 100644 Binary files a/media/css/sf_font2/seafile-font2.eot and b/media/css/sf_font2/seafile-font2.eot differ diff --git a/media/css/sf_font2/seafile-font2.ttf b/media/css/sf_font2/seafile-font2.ttf index 61c9372204..3e834e1ab1 100644 Binary files a/media/css/sf_font2/seafile-font2.ttf and b/media/css/sf_font2/seafile-font2.ttf differ diff --git a/media/css/sf_font2/seafile-font2.woff b/media/css/sf_font2/seafile-font2.woff index c4e8ad7d0d..f805b5ae0d 100644 Binary files a/media/css/sf_font2/seafile-font2.woff and b/media/css/sf_font2/seafile-font2.woff differ diff --git a/seahub/api2/endpoints/groups.py b/seahub/api2/endpoints/groups.py index 4212ee3770..b9ef0015f7 100644 --- a/seahub/api2/endpoints/groups.py +++ b/seahub/api2/endpoints/groups.py @@ -20,9 +20,10 @@ from seahub.api2.throttling import UserRateThrottle from seahub.avatar.settings import GROUP_AVATAR_DEFAULT_SIZE from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \ get_default_group_avatar_url -from seahub.utils import is_org_context +from seahub.utils import is_org_context, is_valid_username from seahub.utils.timeutils import dt, utc_to_local from seahub.group.utils import validate_group_name, check_group_name_conflict +from seahub.group.views import remove_group_common from seahub.base.templatetags.seahub_tags import email2nickname, \ translate_seahub_time @@ -167,3 +168,131 @@ class Groups(APIView): "admins": self._get_group_admins(g.id), } return Response(new_group, status=status.HTTP_201_CREATED) + + def put(self, request, group_id): + """ Rename, transfer a group + """ + + group_id = int(group_id) + try: + group = seaserv.get_group(group_id) + except SearpcError as e: + logger.error(e) + error_msg = _(u'Internal Server Error') + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not group: + error_msg = _(u'Group does not exist.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # only group staff can rename or transfer a group + username = request.user.username + if not seaserv.check_group_staff(group_id, username): + error_msg = _(u'Permission denied') + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + operation = request.DATA.get('operation', '') + if operation.lower() == 'rename': + new_group_name = request.DATA.get('new_group_name', None) + if not new_group_name: + error_msg = _(u'Argument missing') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # Check whether group name is validate. + if not validate_group_name(new_group_name): + error_msg = _(u'Group name can only contain letters, numbers, blank, hyphen or underscore') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # Check whether group name is duplicated. + if check_group_name_conflict(request, new_group_name): + error_msg = _(u'There is already a group with that name.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + seaserv.ccnet_threaded_rpc.set_group_name(group_id, new_group_name) + except SearpcError as e: + logger.error(e) + error_msg = _(u'Internal Server Error') + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + elif operation.lower() == 'transfer': + email = request.DATA.get('email') + if not email: + error_msg = _(u'Argument missing') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if not is_valid_username(email): + error_msg = _('Email %s is not valid.') % email + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if email == group.creator_name: + error_msg = _('%s is already group owner') % email + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + if not seaserv.is_group_user(group_id, email): + seaserv.ccnet_threaded_rpc.group_add_member(group_id, username, email) + + if not seaserv.check_group_staff(group_id, email): + seaserv.ccnet_threaded_rpc.group_set_admin(group_id, email) + + seaserv.ccnet_threaded_rpc.set_group_creator(group_id, email) + except SearpcError as e: + logger.error(e) + error_msg = _(u'Internal Server Error') + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + else: + error_msg = _(u'Operation can only be rename or transfer.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + g = seaserv.get_group(group_id) + val = utc_to_local(dt(g.timestamp)) + avatar_url, is_default, date_uploaded = api_grp_avatar_url(group_id, + GROUP_AVATAR_DEFAULT_SIZE) + + group_info = { + "id": g.id, + "name": g.group_name, + "creator": g.creator_name, + "created_at": val.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(val).format('O'), + "avatar_url": request.build_absolute_uri(avatar_url), + "admins": self._get_group_admins(g.id), + } + return Response(group_info, status=status.HTTP_200_OK) + + def delete(self, request, group_id): + """ Delete a group + """ + + group_id = int(group_id) + try: + group = seaserv.get_group(group_id) + except SearpcError as e: + logger.error(e) + error_msg = _(u'Internal Server Error') + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not group: + error_msg = _(u'Group does not exist.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # only group staff can delete a group + username = request.user.username + if not seaserv.check_group_staff(group_id, username): + error_msg = _(u'Permission denied') + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + org_id = None + if is_org_context(request): + org_id = request.user.org.org_id + + try: + remove_group_common(group_id, username, org_id=org_id) + except SearpcError as e: + logger.error(e) + error_msg = _(u'Internal Server Error') + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}, status=status.HTTP_200_OK) + diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 5972ba995e..ff72a3ad12 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -38,6 +38,8 @@ from .utils import is_repo_writable, is_repo_accessible, \ get_person_msgs, api_group_check, get_email, get_timestamp, \ get_group_message_json, get_group_msgs, get_group_msgs_json, get_diff_details, \ json_response, to_python_boolean, is_seafile_pro + +from seahub.avatar.settings import AVATAR_DEFAULT_SIZE from seahub.avatar.templatetags.avatar_tags import api_avatar_url, avatar from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \ grp_avatar @@ -3468,10 +3470,64 @@ class Groups(APIView): "Operation can only be rename.") class GroupMembers(APIView): - authentication_classes = (TokenAuthentication, ) + authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) throttle_classes = (UserRateThrottle, ) + def get(self, request, group_id, format=None): + """ + Get group members. + """ + try: + group_id_int = int(group_id) + except ValueError: + return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid group ID') + + try: + avatar_size = int(request.GET.get('avatar_size', + AVATAR_DEFAULT_SIZE)) + except ValueError: + avatar_size = AVATAR_DEFAULT_SIZE + + try: + group = seaserv.get_group(group_id_int) + except SearpcError as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, + 'Failed to get group.') + if not group: + return api_error(status.HTTP_404_NOT_FOUND, 'Group not found') + + try: + is_group_user = seaserv.is_group_user(group_id_int, + request.user.username) + except SearpcError as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, + 'Failed to check if is group user.') + if not is_group_user: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + try: + members = seaserv.get_group_members(group.id) + except SearpcError as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, + 'Failed to get group members.') + members_json = [] + for m in members: + avatar_url, is_default, date_uploaded = api_avatar_url(m.user_name, + avatar_size) + + members_json.append({ + 'username': m.user_name, + "fullname": email2nickname(m.user_name), + "avatar_url": request.build_absolute_uri(avatar_url), + }) + + return HttpResponse(json.dumps(members_json), + content_type=json_content_type) + def put(self, request, group_id, format=None): """ Add group members. diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 11480b42f0..78fb53ef68 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -50,9 +50,9 @@ def base(request): except AttributeError: base_template = 'myhome_base.html' - # get 8 user groups try: - grps = request.user.joined_groups[:8] + grps = request.user.joined_groups + grps.sort(lambda x, y: cmp(x.group_name.lower(), y.group_name.lower())) except AttributeError: # anonymous user grps = None diff --git a/seahub/group/templates/group/group_members.html b/seahub/group/templates/group/group_members.html deleted file mode 100644 index 4812526b37..0000000000 --- a/seahub/group/templates/group/group_members.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'group/group_base.html' %} - -{% load seahub_tags avatar_tags i18n %} -{% block cur_members %}tab-cur{%endblock%} - -{% block right_panel %} -
{{ e }}
-