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 "Inviter" %} | +{% trans "Accepter" %} | +{% trans "Type" %} | +{% trans "Invited at" %} | +{% trans "Accepted at" %} | +|
---|---|---|---|---|---|
{{ invitation.inviter }} | +{{ invitation.accepter }} | +{{ invitation.invite_type }} | +{{ invitation.invite_time|translate_seahub_time }} | + {% if invitation.accept_time %} +{{ invitation.accept_time|translate_seahub_time }} | + {% else %} +-- | + {% endif %} +
{% 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