diff --git a/media/css/seahub.css b/media/css/seahub.css index 04530181fd..e1f961b752 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -385,6 +385,7 @@ table img { vertical-align:middle; } .vh { visibility:hidden; } .vam { vertical-align:middle; } .tip { color:#808080; font-size:12px; } +.tick-green { color:green; } .txt-before-btn { margin-bottom:10px; } /* e.g. settings page */ .strip-tip { padding:3px 0; @@ -3561,6 +3562,7 @@ img.thumbnail { padding-top: 20px; text-align: center; } + /* two-factor-auth */ .two-factor-auth-wizard-btns { margin-top:8px; diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py index f678493659..3927b12d94 100644 --- a/seahub/api2/endpoints/invitations.py +++ b/seahub/api2/endpoints/invitations.py @@ -28,9 +28,7 @@ class InvitationsView(APIView): for e in Invitation.objects.get_by_inviter(username): invitations.append(e.to_dict()) - return Response({ - "invitations": invitations - }) + return Response(invitations) def post(self, request, format=None): # Send a invitation. @@ -60,7 +58,4 @@ class InvitationsView(APIView): accepter=accepter) i.send_to(email=accepter) - return Response({ - "accepter_exists": user_exists, - "invitation": i.to_dict() - }, status=201) + return Response(i.to_dict(), status=201) diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py index 3b1fc2b4ce..71e02193d6 100644 --- a/seahub/invitations/models.py +++ b/seahub/invitations/models.py @@ -25,7 +25,8 @@ class InvitationManager(models.Manager): return i def get_by_inviter(self, inviter): - return super(InvitationManager, self).filter(inviter=inviter) + return super(InvitationManager, + self).filter(inviter=inviter).order_by('-invite_time') class Invitation(models.Model): INVITE_TYPE_CHOICES = ( diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 7550319f4b..d7e7613529 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -694,6 +694,11 @@
  • {% trans "Linked Devices" %}
  • + {% if user.permissions.can_invite_guest %} +
  • + {% trans "Invite People" %} +
  • + {% endif %}

    {% trans "Share Admin" %}

    @@ -1655,8 +1660,8 @@ -- <% } %> - - + + @@ -1677,3 +1682,51 @@ + + + + + + diff --git a/static/scripts/app/collections/invitations.js b/static/scripts/app/collections/invitations.js new file mode 100644 index 0000000000..4cfd60e6ce --- /dev/null +++ b/static/scripts/app/collections/invitations.js @@ -0,0 +1,17 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'app/models/invitation' +], function(_, Backbone, Common, Invitation) { + 'use strict'; + + var Invitations = Backbone.Collection.extend({ + model: Invitation, + url: function () { + return Common.getUrl({name: 'invitations'}); + } + }); + + return Invitations; +}); diff --git a/static/scripts/app/models/invitation.js b/static/scripts/app/models/invitation.js new file mode 100644 index 0000000000..88c015cd83 --- /dev/null +++ b/static/scripts/app/models/invitation.js @@ -0,0 +1,11 @@ +define([ + 'underscore', + 'backbone', + 'common', +], function(_, Backbone, Common) { + 'use strict'; + + var Invitation = Backbone.Model.extend({}); + + return Invitation; +}); diff --git a/static/scripts/app/router.js b/static/scripts/app/router.js index dbd656d4b8..f17e658b96 100644 --- a/static/scripts/app/router.js +++ b/static/scripts/app/router.js @@ -13,6 +13,7 @@ define([ 'app/views/starred-file', 'app/views/activities', 'app/views/devices', + 'app/views/invitations', 'app/views/share-admin-repos', 'app/views/share-admin-folders', 'app/views/share-admin-share-links', @@ -21,8 +22,8 @@ define([ 'app/views/account' ], function($, Backbone, Common, SideNavView, MyReposView, SharedReposView, GroupsView, GroupView, OrgView, DirView, - StarredFileView, ActivitiesView, DevicesView, ShareAdminReposView, - ShareAdminFoldersView, ShareAdminShareLinksView, + StarredFileView, ActivitiesView, DevicesView, InvitationsView, + ShareAdminReposView, ShareAdminFoldersView, ShareAdminShareLinksView, ShareAdminUploadLinksView, NotificationsView, AccountView) { "use strict"; @@ -43,6 +44,7 @@ define([ 'starred/': 'showStarredFile', 'activities/': 'showActivities', 'devices/': 'showDevices', + 'invitations/': 'showInvitations', 'share-admin-libs/': 'showShareAdminRepos', 'share-admin-folders/': 'showShareAdminFolders', 'share-admin-share-links/': 'showShareAdminShareLinks', @@ -70,6 +72,7 @@ define([ this.groupsView = new GroupsView(); this.starredFileView = new StarredFileView(); this.devicesView = new DevicesView(); + this.invitationsView = new InvitationsView(); this.activitiesView = new ActivitiesView(); this.shareAdminReposView = new ShareAdminReposView(); this.shareAdminFoldersView = new ShareAdminFoldersView(); @@ -229,6 +232,12 @@ define([ this.sideNavView.setCurTab('devices'); }, + showInvitations: function() { + this.switchCurrentView(this.invitationsView); + this.invitationsView.show(); + this.sideNavView.setCurTab('invitations'); + }, + showShareAdminRepos: function() { this.switchCurrentView(this.shareAdminReposView); this.shareAdminReposView.show(); diff --git a/static/scripts/app/views/invitation.js b/static/scripts/app/views/invitation.js new file mode 100644 index 0000000000..61b900db77 --- /dev/null +++ b/static/scripts/app/views/invitation.js @@ -0,0 +1,66 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'moment', + 'app/views/widgets/hl-item-view' +], function($, _, Backbone, Common, Moment, HLItemView) { + 'use strict'; + + var InvitationView = HLItemView.extend({ + tagName: 'tr', + + template: _.template($('#invitation-item-tmpl').html()), + + initialize: function() { + HLItemView.prototype.initialize.call(this); + }, + + events: { + 'click .rm-invitation': 'removeInvitation', + }, + + removeInvitation: function() { + var _this = this; + + $.ajax({ + url: Common.getUrl({ + 'name': 'invitation', + 'token': this.model.get('token') + }), + type: 'DELETE', + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.remove(); + Common.feedback(gettext("Successfully deleted 1 item"), 'success'); + }, + error: function(xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + + render: function() { + var data = this.model.toJSON(); + + var invite_time = Moment(data['invite_time']); + var expire_time = Moment(data['expire_time']); + + data['invite_time_format'] = invite_time.format('LLLL'); + data['expire_time_format'] = expire_time.format('LLLL'); + + data['invite_time_from_now'] = invite_time.format('YYYY-MM-DD'); + data['expire_time_from_now'] = expire_time.format('YYYY-MM-DD'); + + this.$el.html(this.template(data)); + + return this; + } + + }); + + return InvitationView; +}); diff --git a/static/scripts/app/views/invitations.js b/static/scripts/app/views/invitations.js new file mode 100644 index 0000000000..b8a9d7fc3c --- /dev/null +++ b/static/scripts/app/views/invitations.js @@ -0,0 +1,149 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/views/invitation', + 'app/collections/invitations', +], function($, _, Backbone, Common, InvitedPeopleView, + InvitedPeopleCollection) { + + 'use strict'; + + var InvitationsView = Backbone.View.extend({ + + id: 'invited_peoples', + + template: _.template($('#invitations-tmpl').html()), + invitePeopleFormTemplate: _.template($('#invitation-form-tmpl').html()), + + initialize: function() { + this.invitedPeoples = new InvitedPeopleCollection(); + this.listenTo(this.invitedPeoples, 'reset', this.reset); + this.render(); + }, + + events: { + 'click #invite-people': 'invitePeople' + }, + + invitePeople: function() { + var form = $(this.invitePeopleFormTemplate()), + form_id = form.attr('id'), + _this = this; + + form.modal({appendTo:'#main'}); + $('#simplemodal-container').css({'height':'auto'}); + + form.submit(function() { + var accepter = $.trim($('input[name="accepter"]', form).val()); + + if (!accepter) { + Common.showFormError(form_id, gettext("It is required.")); + return false; + }; + + var post_data = {'type': 'guest', 'accepter': accepter}, + post_url = Common.getUrl({name: "invitations"}); + + var after_op_success = function(data) { + $.modal.close(); + var new_people_invited = _this.invitedPeoples.add({ + 'accepter': data['accepter'], + 'type': data['type'], + 'accept_time': '', + 'invite_time': data['invite_time'], + 'expire_time': data['expire_time'] + }, {silent:true}); + + var view = new InvitedPeopleView({model: new_people_invited}); + _this.$tableBody.prepend(view.render().el); + }; + + Common.ajaxPost({ + 'form': form, + 'post_url': post_url, + 'post_data': post_data, + 'after_op_success': after_op_success, + 'form_id': form_id + }); + + return false; + }); + }, + + addOne: function(invitedPeople) { + var view = new InvitedPeopleView({model: invitedPeople}); + this.$tableBody.append(view.render().el); + }, + + initPage: function() { + this.$table.hide(); + this.$tableBody.empty(); + this.$loadingTip.show(); + this.$emptyTip.hide(); + }, + + reset: function() { + this.$tableBody.empty(); + this.$loadingTip.hide(); + this.invitedPeoples.each(this.addOne, this); + if (this.invitedPeoples.length) { + this.$emptyTip.hide(); + this.$table.show(); + } else { + this.$emptyTip.show(); + this.$table.hide(); + } + }, + + render: function() { + this.$el.html(this.template()); + this.$tip = this.$('.tip'); + this.$table = this.$('table'); + this.$tableBody = $('tbody', this.$table); + this.$loadingTip = this.$('.loading-tip'); + this.$emptyTip = this.$('.empty-tips'); + }, + + show: function() { + if (!this.attached) { + this.attached = true; + $("#right-panel").html(this.$el); + } + this.showInvitedPeoples(); + }, + + showInvitedPeoples: function() { + this.initPage(); + var _this = this; + + this.invitedPeoples.fetch({ + 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'); + } + }); + }, + + hide: function() { + this.$el.detach(); + this.attached = false; + } + + }); + + return InvitationsView; +}); diff --git a/static/scripts/common.js b/static/scripts/common.js index 972dea7a4d..c25c7979e3 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -160,8 +160,10 @@ define([ case 'set_notice_seen_by_id': return siteRoot + 'ajax/set_notice_seen_by_id/'; case 'toggle_personal_modules': return siteRoot + 'ajax/toggle-personal-modules/'; case 'starred_files': return siteRoot + 'api2/starredfiles/'; - case 'devices': return siteRoot + 'api2/devices/'; case 'events': return siteRoot + 'api2/events/'; + case 'devices': return siteRoot + 'api2/devices/'; + case 'invitations': return siteRoot + 'api/v2.1/invitations/'; + case 'invitation': return siteRoot + 'api/v2.1/invitations/' + options.token + '/'; case 'search_user': return siteRoot + 'api2/search-user/'; case 'user_profile': return siteRoot + 'profile/' + options.username + '/'; case 'space_and_traffic': return siteRoot + 'ajax/space_and_traffic/'; diff --git a/tests/api/endpoints/test_invitations.py b/tests/api/endpoints/test_invitations.py index 9461705527..8b89b9b5d1 100644 --- a/tests/api/endpoints/test_invitations.py +++ b/tests/api/endpoints/test_invitations.py @@ -26,10 +26,9 @@ class InvitationsTest(BaseTestCase): self.assertEqual(201, resp.status_code) json_resp = json.loads(resp.content) - assert json_resp['accepter_exists'] is False - assert json_resp['invitation']['inviter'] == self.username - assert json_resp['invitation']['accepter'] == 'some_random_user@1.com' - assert json_resp['invitation']['expire_time'] is not None + assert json_resp['inviter'] == self.username + assert json_resp['accepter'] == 'some_random_user@1.com' + assert json_resp['expire_time'] is not None assert len(Invitation.objects.all()) == 1 @@ -48,7 +47,7 @@ class InvitationsTest(BaseTestCase): self.assertEqual(len(Email.objects.all()), 1) self.assertRegexpMatches(Email.objects.all()[0].html_message, - json_resp['invitation']['token']) + json_resp['token']) @patch.object(UserPermissions, 'can_invite_guest') def test_can_list(self, mock_can_invite_guest): @@ -60,4 +59,4 @@ class InvitationsTest(BaseTestCase): resp = self.client.get(self.endpoint) self.assertEqual(200, resp.status_code) json_resp = json.loads(resp.content) - assert len(json_resp['invitations']) == 2 + assert len(json_resp) == 2