diff --git a/seahub/api2/endpoints/invitation.py b/seahub/api2/endpoints/invitation.py new file mode 100644 index 0000000000..f78ee90ef7 --- /dev/null +++ b/seahub/api2/endpoints/invitation.py @@ -0,0 +1,51 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.permissions import CanInviteGuest +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.invitations.models import Invitation + +json_content_type = 'application/json; charset=utf-8' + +def invitation_owner_check(func): + """Check whether user is the invitation inviter. + """ + def _decorated(view, request, token, *args, **kwargs): + i = get_object_or_404(Invitation, token=token) + if i.inviter != request.user.username: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + return func(view, request, i, *args, **kwargs) + + return _decorated + +class InvitationView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanInviteGuest) + throttle_classes = (UserRateThrottle, ) + + @invitation_owner_check + def get(self, request, invitation, format=None): + # Get a certain invitation. + return Response(invitation.to_dict()) + + # @invitation_owner_check + # def put(self, request, invitation, format=None): + # # Update an invitation. + # # TODO + # return Response({ + # }, status=200) + + @invitation_owner_check + def delete(self, request, invitation, format=None): + # Delete an invitation. + invitation.delete() + + return Response({ + }, status=204) diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py new file mode 100644 index 0000000000..f678493659 --- /dev/null +++ b/seahub/api2/endpoints/invitations.py @@ -0,0 +1,66 @@ +from django.utils.translation import ugettext as _ +from rest_framework import status +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.permissions import CanInviteGuest +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.base.accounts import User +from seahub.invitations.models import Invitation +from seahub.utils import is_valid_email + +json_content_type = 'application/json; charset=utf-8' + +class InvitationsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanInviteGuest) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, format=None): + # List invitations sent by user. + username = request.user.username + + invitations = [] + for e in Invitation.objects.get_by_inviter(username): + invitations.append(e.to_dict()) + + return Response({ + "invitations": invitations + }) + + def post(self, request, format=None): + # Send a invitation. + itype = request.data.get('type', '').lower() + if not itype or itype != 'guest': + return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.') + + accepter = request.data.get('accepter', '').lower() + if not accepter: + return api_error(status.HTTP_400_BAD_REQUEST, 'accepter invalid.') + + if not is_valid_email(accepter): + return api_error(status.HTTP_400_BAD_REQUEST, + _('Email %s invalid.') % accepter) + + try: + User.objects.get(accepter) + user_exists = True + except User.DoesNotExist: + user_exists = False + + if user_exists: + return api_error(status.HTTP_400_BAD_REQUEST, + _('User %s already exists.') % accepter) + + i = Invitation.objects.add(inviter=request.user.username, + accepter=accepter) + i.send_to(email=accepter) + + return Response({ + "accepter_exists": user_exists, + "invitation": i.to_dict() + }, status=201) diff --git a/seahub/api2/permissions.py b/seahub/api2/permissions.py index b4c0e57c84..5d030906a2 100644 --- a/seahub/api2/permissions.py +++ b/seahub/api2/permissions.py @@ -53,3 +53,10 @@ class IsGroupMember(BasePermission): group_id = int(view.kwargs.get('group_id', '')) username = request.user.username if request.user else '' return True if ccnet_api.is_group_user(group_id, username) else False + + +class CanInviteGuest(BasePermission): + """Check user has permission to invite a guest. + """ + def has_permission(self, request, *args, **kwargs): + return request.user.permissions.can_invite_guest() diff --git a/seahub/invitations/__init__.py b/seahub/invitations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/invitations/admin.py b/seahub/invitations/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/seahub/invitations/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/seahub/invitations/migrations/0001_initial.py b/seahub/invitations/migrations/0001_initial.py new file mode 100644 index 0000000000..cefc33955c --- /dev/null +++ b/seahub/invitations/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import seahub.base.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Invitation', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('token', models.CharField(max_length=40)), + ('inviter', seahub.base.fields.LowerCaseCharField(max_length=255, db_index=True)), + ('acceptor', seahub.base.fields.LowerCaseCharField(max_length=255)), + ('invite_time', models.DateTimeField(auto_now_add=True)), + ('accept_time', models.DateTimeField(null=True, blank=True)), + ], + ), + ] diff --git a/seahub/invitations/migrations/0002_invitation_invite_type.py b/seahub/invitations/migrations/0002_invitation_invite_type.py new file mode 100644 index 0000000000..448fce1ab6 --- /dev/null +++ b/seahub/invitations/migrations/0002_invitation_invite_type.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='invitation', + name='invite_type', + field=models.CharField(default=b'guest', max_length=20, choices=[(b'guest', b'guest')]), + ), + ] diff --git a/seahub/invitations/migrations/0003_auto_20160510_1703.py b/seahub/invitations/migrations/0003_auto_20160510_1703.py new file mode 100644 index 0000000000..2d0c4405bd --- /dev/null +++ b/seahub/invitations/migrations/0003_auto_20160510_1703.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0002_invitation_invite_type'), + ] + + operations = [ + migrations.RenameField( + model_name='invitation', + old_name='acceptor', + new_name='accepter', + ), + migrations.AlterField( + model_name='invitation', + name='token', + field=models.CharField(max_length=40, db_index=True), + ), + ] diff --git a/seahub/invitations/migrations/0004_auto_20160629_1610.py b/seahub/invitations/migrations/0004_auto_20160629_1610.py new file mode 100644 index 0000000000..46983ed2bf --- /dev/null +++ b/seahub/invitations/migrations/0004_auto_20160629_1610.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0003_auto_20160510_1703'), + ] + + operations = [ + migrations.AddField( + model_name='invitation', + name='expire_date', + field=models.DateTimeField(default=datetime.datetime(2016, 6, 29, 16, 10, 45, 816971)), + preserve_default=False, + ), + migrations.AlterField( + model_name='invitation', + name='invite_type', + field=models.CharField(default='Guest', max_length=20, choices=[('Guest', 'Guest')]), + ), + ] diff --git a/seahub/invitations/migrations/0005_auto_20160629_1614.py b/seahub/invitations/migrations/0005_auto_20160629_1614.py new file mode 100644 index 0000000000..da90155f17 --- /dev/null +++ b/seahub/invitations/migrations/0005_auto_20160629_1614.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0004_auto_20160629_1610'), + ] + + operations = [ + migrations.RenameField( + model_name='invitation', + old_name='expire_date', + new_name='expire_time', + ), + ] diff --git a/seahub/invitations/migrations/__init__.py b/seahub/invitations/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py new file mode 100644 index 0000000000..3b1fc2b4ce --- /dev/null +++ b/seahub/invitations/models.py @@ -0,0 +1,94 @@ +from datetime import timedelta + +from django.db import models +from django.template.loader import render_to_string +from django.utils import timezone +from django.utils.translation import ugettext as _ + +from seahub.base.fields import LowerCaseCharField +from seahub.invitations.settings import INVITATIONS_TOKEN_AGE +from seahub.utils import gen_token +from seahub.utils.timeutils import datetime_to_isoformat_timestr +from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.settings import SITE_NAME + +GUEST = _('Guest') + +class InvitationManager(models.Manager): + def add(self, inviter, accepter, invite_type=GUEST): + token = gen_token(max_length=32) + expire_at = timezone.now() + timedelta(hours=INVITATIONS_TOKEN_AGE) + + i = self.model(token=token, inviter=inviter, accepter=accepter, + invite_type=invite_type, expire_time=expire_at) + i.save(using=self._db) + return i + + def get_by_inviter(self, inviter): + return super(InvitationManager, self).filter(inviter=inviter) + +class Invitation(models.Model): + INVITE_TYPE_CHOICES = ( + (GUEST, _('Guest')), + ) + + token = models.CharField(max_length=40, db_index=True) + inviter = LowerCaseCharField(max_length=255, db_index=True) + accepter = LowerCaseCharField(max_length=255) + invite_type = models.CharField(max_length=20, + choices=INVITE_TYPE_CHOICES, + default=GUEST) + invite_time = models.DateTimeField(auto_now_add=True) + accept_time = models.DateTimeField(null=True, blank=True) + expire_time = models.DateTimeField() + objects = InvitationManager() + + def __unicode__(self): + return "Invitation from %s on %s (%s)" % ( + self.inviter, self.invite_time, self.token) + + def accept(self): + self.accept_time = timezone.now() + self.save() + + def to_dict(self): + accept_time = datetime_to_isoformat_timestr(self.accept_time) \ + if self.accept_time else "" + return { + "id": self.pk, + "token": self.token, + "inviter": self.inviter, + "accepter": self.accepter, + "type": self.invite_type, + "invite_time": datetime_to_isoformat_timestr(self.invite_time), + "accept_time": accept_time, + "expire_time": datetime_to_isoformat_timestr(self.expire_time), + } + + def is_guest(self): + return self.invite_type == GUEST + + def is_expired(self): + return timezone.now() >= self.expire_time + + def send_to(self, email=None): + """ + Send an invitation email to ``email``. + """ + if not email: + email = self.accepter + + context = { + 'inviter': self.inviter, + 'site_name': SITE_NAME, + 'token': self.token, + } + subject = render_to_string('invitations/invitation_email_subject.txt', + context) + + send_html_email_with_dj_template( + email, dj_template='invitations/invitation_email.html', + context=context, + subject=subject, + priority=MAIL_PRIORITY.now + ) diff --git a/seahub/invitations/settings.py b/seahub/invitations/settings.py new file mode 100644 index 0000000000..264bdafa75 --- /dev/null +++ b/seahub/invitations/settings.py @@ -0,0 +1,3 @@ +from django.conf import settings + +INVITATIONS_TOKEN_AGE = getattr(settings, 'INVITATIONS_TOKEN_AGE', 72) # hours diff --git a/seahub/invitations/templates/invitations/invitation_email.html b/seahub/invitations/templates/invitations/invitation_email.html new file mode 100644 index 0000000000..77bf27dff6 --- /dev/null +++ b/seahub/invitations/templates/invitations/invitation_email.html @@ -0,0 +1,19 @@ +{% extends 'email_base.html' %} + +{% load i18n %} + +{% block email_con %} + +{% autoescape off %} + +

{% trans "Hi," %}

+ +

+{% blocktrans %}{{ inviter }} invites you to join {{ site_name }}. Please click the link below:{% endblocktrans %} +

+ +{{ url_base }}{% url 'invitations:token_view' token %} + +{% endautoescape %} + +{% endblock %} diff --git a/seahub/invitations/templates/invitations/invitation_email_subject.txt b/seahub/invitations/templates/invitations/invitation_email_subject.txt new file mode 100644 index 0000000000..74f94ce1c2 --- /dev/null +++ b/seahub/invitations/templates/invitations/invitation_email_subject.txt @@ -0,0 +1 @@ +{% load i18n%}{% blocktrans %}{{ inviter }} invite you to join {{ site_name}}{% endblocktrans %} diff --git a/seahub/invitations/templates/invitations/token_view.html b/seahub/invitations/templates/invitations/token_view.html new file mode 100644 index 0000000000..5bfdccd4de --- /dev/null +++ b/seahub/invitations/templates/invitations/token_view.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% load i18n %} +{% block sub_title %}{% trans "Create Account" %}{% endblock %} + +{% block extra_style %} + +{% endblock %} + +{% block main_panel %} +
+

{% trans "Set your password" %}

+
{% csrf_token %} +
+ + + + +

+ +
+
+{% endblock %} + +{% block extra_script %} + + +{% endblock %} diff --git a/seahub/invitations/urls.py b/seahub/invitations/urls.py new file mode 100644 index 0000000000..6f0e168d6e --- /dev/null +++ b/seahub/invitations/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import patterns, url + +from .views import token_view + +urlpatterns = patterns( + '', + url(r'^token/(?P[a-f0-9]{32})/$', token_view, name='token_view') +) diff --git a/seahub/invitations/views.py b/seahub/invitations/views.py new file mode 100644 index 0000000000..47800bf650 --- /dev/null +++ b/seahub/invitations/views.py @@ -0,0 +1,45 @@ +from django.contrib import messages +from django.http import HttpResponseRedirect, Http404 +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext +from django.utils.translation import ugettext as _ + +from seahub.auth import login as auth_login +from seahub.auth import get_backends +from seahub.base.accounts import User +from seahub.constants import GUEST_USER +from seahub.invitations.models import Invitation +from seahub.settings import SITE_ROOT + +def token_view(request, token): + """Show form to let user set password. + """ + i = get_object_or_404(Invitation, token=token) + if i.is_expired(): + raise Http404 + + if request.method == 'POST': + passwd = request.POST.get('password', '') + if not passwd: + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + + try: + User.objects.get(email=i.accepter) + messages.error(request, _('A user with this email already exists.')) + except User.DoesNotExist: + # Create user, set that user as guest, and log user in. + u = User.objects.create_user(email=i.accepter, password=passwd, + is_active=True) + u.role = GUEST_USER + u.save() + + i.accept() # Update invitaion accept time. + + for backend in get_backends(): + u.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) + auth_login(request, u) + return HttpResponseRedirect(SITE_ROOT) + + return render_to_response('invitations/token_view.html', { + 'iv': i, + }, context_instance=RequestContext(request)) diff --git a/seahub/settings.py b/seahub/settings.py index f812f6ddfd..4e0bbe6f3e 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -200,6 +200,7 @@ INSTALLED_APPS = ( 'seahub.base', 'seahub.contacts', 'seahub.institutions', + 'seahub.invitations', 'seahub.wiki', 'seahub.group', 'seahub.message', diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index 59350b0481..ba2ca8d462 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -62,6 +62,10 @@ {% trans "Virus Scan" %} {% endif %} +
  • + {% trans "Invitations" %} +
  • + {% endblock %} diff --git a/seahub/templates/sysadmin/sys_invitations_admin.html b/seahub/templates/sysadmin/sys_invitations_admin.html new file mode 100644 index 0000000000..667c3d997c --- /dev/null +++ b/seahub/templates/sysadmin/sys_invitations_admin.html @@ -0,0 +1,42 @@ +{% extends "sysadmin/base.html" %} +{% load i18n seahub_tags %} + +{% block cur_invitations %}tab-cur{% endblock %} + +{% block right_panel %} +

    {% trans "All Invitations" %}

    + +{% if invitations %} + + + + + + + + + {% for invitation in invitations %} + + + + + + {% if invitation.accept_time %} + + {% else %} + + {% endif %} + + {% endfor %} +
    {% trans "Inviter" %}{% trans "Accepter" %}{% trans "Type" %}{% trans "Invited at" %}{% trans "Accepted at" %}
    {{ invitation.inviter }}{{ invitation.accepter }}{{ invitation.invite_type }}{{ invitation.invite_time|translate_seahub_time }} {{ invitation.accept_time|translate_seahub_time }}--
    + +{% 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 448affccc9..42af9ab014 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -41,6 +41,8 @@ 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 # Uncomment the next two lines to enable the admin: #from django.contrib import admin @@ -198,6 +200,8 @@ urlpatterns = patterns( url(r'^api/v2.1/admin/sysinfo/$', SysInfo.as_view(), name='api-v2.1-sysinfo'), url(r'^api/v2.1/admin/devices/$', AdminDevices.as_view(), name='api-v2.1-admin-devices'), url(r'^api/v2.1/admin/device-errors/$', AdminDeviceErrors.as_view(), name='api-v2.1-admin-device-errors'), + url(r'^api/v2.1/invitations/$', InvitationsView.as_view()), + url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/$', InvitationView.as_view()), 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'), @@ -219,6 +223,7 @@ urlpatterns = patterns( url(r'^captcha/', include('captcha.urls')), (r'^thumbnail/', include('seahub.thumbnail.urls')), url(r'^inst/', include('seahub.institutions.urls', app_name='institutions', namespace='institutions')), + url(r'^invite/', include('seahub.invitations.urls', app_name='invitations', namespace='invitations')), ### system admin ### url(r'^sysadmin/$', sysadmin, name='sysadmin'), @@ -260,6 +265,7 @@ urlpatterns = patterns( url(r'^sys/publink/remove/$', sys_publink_remove, name='sys_publink_remove'), url(r'^sys/uploadlink/remove/$', sys_upload_link_remove, name='sys_upload_link_remove'), url(r'^sys/notificationadmin/', notification_list, name='notification_list'), + url(r'^sys/invitationadmin/$', sys_invitation_admin, name='sys_invitaion_admin'), url(r'^sys/sudo/', sys_sudo_mode, name='sys_sudo_mode'), url(r'^sys/check-license/', sys_check_license, name='sys_check_license'), url(r'^useradmin/add/$', user_add, name="user_add"), diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 6433521a32..128647a2b8 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -34,6 +34,7 @@ from seahub.auth import authenticate from seahub.auth.decorators import login_required, login_required_ajax from seahub.constants import GUEST_USER, DEFAULT_USER from seahub.institutions.models import Institution, InstitutionAdmin +from seahub.invitations.models import Invitation from seahub.role_permissions.utils import get_available_roles from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \ is_pro_version, send_html_email, get_user_traffic_list, get_server_id, \ @@ -2240,3 +2241,36 @@ def sys_inst_toggle_admin(request, inst_id, email): messages.success(request, _('Success')) return HttpResponseRedirect(next) + +@login_required +@sys_staff_required +def sys_invitation_admin(request): + """List all invitations . + """ + # 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', '100')) + except ValueError: + current_page = 1 + per_page = 100 + + offset = per_page * (current_page - 1) + limit = per_page + 1 + invitations = Invitation.objects.all()[offset:offset + limit] + + if len(invitations) == per_page + 1: + page_next = True + else: + page_next = False + + return render_to_response( + 'sysadmin/sys_invitations_admin.html', { + 'invitations': invitations, + '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)) diff --git a/tests/api/endpoints/test_invitation.py b/tests/api/endpoints/test_invitation.py new file mode 100644 index 0000000000..24d6db1b19 --- /dev/null +++ b/tests/api/endpoints/test_invitation.py @@ -0,0 +1,60 @@ +import json +from mock import patch + +from django.core.urlresolvers import reverse + +from seahub.base.accounts import UserPermissions +from seahub.invitations.models import Invitation +from seahub.test_utils import BaseTestCase + + +class InvitationsTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.username = self.user.username + + self.i = Invitation.objects.add(inviter=self.username, + accepter='1@1.com') + self.endpoint = '/api/v2.1/invitations/' + self.i.token + '/' + assert len(Invitation.objects.all()) == 1 + + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_get_one(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + resp = self.client.get(self.endpoint) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['inviter'] == self.username + assert json_resp['accepter'] == '1@1.com' + + @patch.object(UserPermissions, 'can_invite_guest') + def test_get_invalid(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + self.i.delete() + assert len(Invitation.objects.all()) == 0 + + resp = self.client.get(self.endpoint) + self.assertEqual(404, resp.status_code) + + @patch.object(UserPermissions, 'can_invite_guest') + def test_get_permission_denied(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + self.logout() + self.login_as(self.admin) + + resp = self.client.get(self.endpoint) + self.assertEqual(403, resp.status_code) + self.logout() + + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_delete(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + resp = self.client.delete(self.endpoint) + self.assertEqual(204, resp.status_code) + + assert len(Invitation.objects.all()) == 0 diff --git a/tests/api/endpoints/test_invitations.py b/tests/api/endpoints/test_invitations.py new file mode 100644 index 0000000000..9461705527 --- /dev/null +++ b/tests/api/endpoints/test_invitations.py @@ -0,0 +1,63 @@ +import json +from mock import patch + +from post_office.models import Email +from django.core.urlresolvers import reverse + +from seahub.base.accounts import UserPermissions +from seahub.invitations.models import Invitation +from seahub.test_utils import BaseTestCase + +class InvitationsTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.endpoint = '/api/v2.1/invitations/' + self.username = self.user.username + + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_add(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + assert len(Invitation.objects.all()) == 0 + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': 'some_random_user@1.com', + }) + 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 len(Invitation.objects.all()) == 1 + + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_send_mail(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + self.assertEqual(len(Email.objects.all()), 0) + + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': 'some_random_user@1.com', + }) + self.assertEqual(201, resp.status_code) + json_resp = json.loads(resp.content) + + self.assertEqual(len(Email.objects.all()), 1) + self.assertRegexpMatches(Email.objects.all()[0].html_message, + json_resp['invitation']['token']) + + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_list(self, mock_can_invite_guest): + mock_can_invite_guest.return_val = True + + Invitation.objects.add(inviter=self.username, accepter='1@1.com') + Invitation.objects.add(inviter=self.username, accepter='1@2.com') + + resp = self.client.get(self.endpoint) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['invitations']) == 2 diff --git a/tests/seahub/invitations/__init__.py b/tests/seahub/invitations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/seahub/invitations/test_models.py b/tests/seahub/invitations/test_models.py new file mode 100644 index 0000000000..a4b5a62005 --- /dev/null +++ b/tests/seahub/invitations/test_models.py @@ -0,0 +1,26 @@ +from django.utils import timezone +from django.test import override_settings + +from seahub.invitations.models import Invitation +from seahub.test_utils import BaseTestCase + + +class InvitationTest(BaseTestCase): + def test_is_expired(self): + i = Invitation.objects.add('f@f.com', 'g@g.com') + assert i.is_expired() is False + + i.expire_time = timezone.now() + i.save() + assert i.is_expired() is True + + +class InvitationManagerTest(BaseTestCase): + def test_can_add(self): + assert len(Invitation.objects.all()) == 0 + + i = Invitation.objects.add('f@f.com', 'g@g.com') + assert i is not None + assert i.is_expired() is False + + assert len(Invitation.objects.all()) == 1 diff --git a/tests/seahub/invitations/test_views.py b/tests/seahub/invitations/test_views.py new file mode 100644 index 0000000000..5b96dfeca9 --- /dev/null +++ b/tests/seahub/invitations/test_views.py @@ -0,0 +1,43 @@ +from django.utils import timezone +from django.core.urlresolvers import reverse + +from seahub.invitations.models import Invitation +from seahub.test_utils import BaseTestCase + + +class TokenViewTest(BaseTestCase): + def setUp(self): + self.accepter = 'random@foo.com' + self.iv = Invitation.objects.add(inviter=self.user.username, + accepter=self.accepter) + self.url = reverse('invitations:token_view', args=[self.iv.token]) + + def tearDown(self): + self.remove_user(self.accepter) + + def test_get(self): + resp = self.client.get(self.url) + self.assertEqual(200, resp.status_code) + self.assertRegexpMatches(resp.content, 'Set your password') + + def test_expired_token(self): + self.iv.expire_time = timezone.now() + self.iv.save() + resp = self.client.get(self.url) + self.assertEqual(404, resp.status_code) + + def test_post(self): + assert self.iv.accept_time is None + resp = self.client.post(self.url, { + 'password': 'passwd' + }) + self.assertEqual(302, resp.status_code) + assert Invitation.objects.get(pk=self.iv.pk).accept_time is not None + + def test_post_empty_password(self): + assert self.iv.accept_time is None + resp = self.client.post(self.url, { + 'password': '', + }) + self.assertEqual(302, resp.status_code) + assert Invitation.objects.get(pk=self.iv.pk).accept_time is None diff --git a/tests/seahub/views/sysadmin/test_sys_invitation_admin.py b/tests/seahub/views/sysadmin/test_sys_invitation_admin.py new file mode 100644 index 0000000000..816183c40f --- /dev/null +++ b/tests/seahub/views/sysadmin/test_sys_invitation_admin.py @@ -0,0 +1,11 @@ +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase + +class SysInvitationAdminTest(BaseTestCase): + def test_can_list(self): + self.login_as(self.admin) + + resp = self.client.get(reverse('sys_invitaion_admin')) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed('sysadmin/sys_invitations_admin.html')