diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 0749c5a8a..6a593fb0b 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -1,9 +1,13 @@ -from datetime import timedelta +from datetime import timedelta, datetime from decimal import Decimal from gettext import gettext as _ +from html import escape +from smtplib import SMTPException from django.contrib.auth.models import User, Group +from django.core.mail import send_mail from django.db.models import Avg, Q, QuerySet, Sum +from django.http import BadHeaderError from django.urls import reverse from django.utils import timezone from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer @@ -1032,13 +1036,35 @@ class InviteLinkSerializer(WritableNestedModelSerializer): def create(self, validated_data): validated_data['created_by'] = self.context['request'].user validated_data['space'] = self.context['request'].space - return super().create(validated_data) + obj = super().create(validated_data) + + if obj.email: + try: + if InviteLink.objects.filter(space=self.context['request'].space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20: + message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.username) + message += _(' to join their Tandoor Recipes space ') + escape(self.context['request'].space.name) + '.\n\n' + message += _('Click the following link to activate your account: ') + self.context['request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n' + message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n' + message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n' + message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/' + + send_mail( + _('Tandoor Recipes Invite'), + message, + None, + [obj.email], + fail_silently=True, + ) + except (SMTPException, BadHeaderError, TimeoutError): + pass + + return obj class Meta: model = InviteLink fields = ( 'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'created_by', 'created_at',) - read_only_fields = ('id', 'uuid', 'email', 'created_by', 'created_at',) + read_only_fields = ('id', 'uuid', 'created_by', 'created_at',) # CORS, REST and Scopes aren't currently working diff --git a/cookbook/views/new.py b/cookbook/views/new.py index d6e28cc82..01478da0f 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -190,59 +190,3 @@ class MealPlanCreate(GroupRequiredMixin, CreateView, SpaceFormMixing): return context -class InviteLinkCreate(GroupRequiredMixin, CreateView): - groups_required = ['admin'] - template_name = "generic/new_template.html" - model = InviteLink - form_class = InviteLinkForm - - def form_valid(self, form): - obj = form.save(commit=False) - obj.created_by = self.request.user - - # verify given space is actually owned by the user creating the link - if obj.space.created_by != self.request.user: - obj.space = self.request.space - obj.save() - if obj.email: - try: - if InviteLink.objects.filter(space=self.request.space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20: - message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.request.user.username) - message += _(' to join their Tandoor Recipes space ') + escape(self.request.space.name) + '.\n\n' - message += _('Click the following link to activate your account: ') + self.request.build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n' - message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n' - message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n' - message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/' - - send_mail( - _('Tandoor Recipes Invite'), - message, - None, - [obj.email], - fail_silently=False, - ) - messages.add_message(self.request, messages.SUCCESS, - _('Invite link successfully send to user.')) - else: - messages.add_message(self.request, messages.ERROR, - _('You have send to many emails, please share the link manually or wait a few hours.')) - except (SMTPException, BadHeaderError, TimeoutError): - messages.add_message(self.request, messages.ERROR, _('Email could not be sent to user. Please share the link manually.')) - - return HttpResponseRedirect(reverse('view_space')) - - def get_context_data(self, **kwargs): - context = super(InviteLinkCreate, self).get_context_data(**kwargs) - context['title'] = _("Invite Link") - return context - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs.update({'user': self.request.user}) - return kwargs - - def get_initial(self): - return dict( - space=self.request.space, - group=Group.objects.get(name='user') - ) diff --git a/vue/src/apps/SpaceManageView/SpaceManageView.vue b/vue/src/apps/SpaceManageView/SpaceManageView.vue index 84eebfb21..b59209126 100644 --- a/vue/src/apps/SpaceManageView/SpaceManageView.vue +++ b/vue/src/apps/SpaceManageView/SpaceManageView.vue @@ -4,16 +4,41 @@
- Recipes {{ space.recipe_count }} / {{ space.max_recipes }} - Users {{ space.user_count }} / {{ space.max_users }} - Files {{ space.file_size_mb }} / {{ space.max_file_storage_mb }} +
{{ $t('Recipes') }}
+ + + {{ space.recipe_count }} / + + + + + +
{{ $t('Users') }}
+ + + {{ space.user_count }} / + + + + + +
{{ $t('Files') }}
+ + + {{ space.file_size_mb }} / + + + + +
-
+
+

{{ $t('Users') }}

@@ -48,15 +73,15 @@
-
+

{{ $t('Invites') }}

- + @@ -75,7 +100,6 @@ :multiple="false" /> -
# {{ $t('Email') }} {{ $t('Group') }}{{ $t('Token') }}
@@ -83,20 +107,20 @@ - - + + + + + + {{ $t('Copy Link') }} - - + + {{ $t('Copy Token') }} - - - - - - {{ $t('Delete') }} + + {{ $t('Delete') }} @@ -105,6 +129,7 @@
+ {{ $t('Create') }}
@@ -154,6 +179,13 @@ export default { this.loadInviteLinks() }, methods: { + copyToClipboard: function (inviteLink, link) { + let content = inviteLink.uuid + if (link) { + content = localStorage.BASE_PATH + this.resolveDjangoUrl('view_invite', inviteLink.uuid) + } + navigator.clipboard.writeText(content) + }, loadInviteLinks: function () { let apiFactory = new ApiApiFactory() apiFactory.listInviteLinks().then(r => { @@ -178,7 +210,17 @@ export default { StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err) }) } - } + }, + deleteInviteLink: function (inviteLink) { + let apiFactory = new ApiApiFactory() + apiFactory.destroyInviteLink(inviteLink.id).then(r => { + this.invite_links = this.invite_links.filter(i => i !== inviteLink) + StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE) + }).catch(err => { + StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err) + }) + + }, }, } diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 91962012a..327ad4c3b 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -125,6 +125,8 @@ "Move": "Move", "Merge": "Merge", "Parent": "Parent", + "Copy Link": "Copy Link", + "Copy Token": "Copy Token", "delete_confirmation": "Are you sure that you want to delete {source}?", "move_confirmation": "Move {child} to parent {parent}", "merge_confirmation": "Replace {source} with {target}", @@ -262,6 +264,8 @@ "New_Cookbook": "New cookbook", "Hide_Keyword": "Hide keywords", "Clear": "Clear", + "Users": "Users", + "Invites": "Invites", "err_move_self": "Cannot move item to itself", "nothing": "Nothing to do", "err_merge_self": "Cannot merge item with itself",