space management page progress
This commit is contained in:
@ -1,9 +1,13 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
from html import escape
|
||||||
|
from smtplib import SMTPException
|
||||||
|
|
||||||
from django.contrib.auth.models import User, Group
|
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.db.models import Avg, Q, QuerySet, Sum
|
||||||
|
from django.http import BadHeaderError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||||
@ -1032,13 +1036,35 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
validated_data['space'] = self.context['request'].space
|
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:
|
class Meta:
|
||||||
model = InviteLink
|
model = InviteLink
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'created_by', 'created_at',)
|
'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
|
# CORS, REST and Scopes aren't currently working
|
||||||
|
@ -190,59 +190,3 @@ class MealPlanCreate(GroupRequiredMixin, CreateView, SpaceFormMixing):
|
|||||||
return context
|
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')
|
|
||||||
)
|
|
||||||
|
@ -4,16 +4,41 @@
|
|||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col col-12">
|
<div class="col col-12">
|
||||||
<div v-if="space !== undefined">
|
<div v-if="space !== undefined">
|
||||||
Recipes {{ space.recipe_count }} / {{ space.max_recipes }}
|
<h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6>
|
||||||
Users {{ space.user_count }} / {{ space.max_users }}
|
<b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true">
|
||||||
Files {{ space.file_size_mb }} / {{ space.max_file_storage_mb }}
|
<b-progress-bar :value="space.recipe_count">
|
||||||
|
{{ space.recipe_count }} /
|
||||||
|
<template v-if="space.max_recipes === 0">∞</template>
|
||||||
|
<template v-else>{{ space.max_recipes }}</template>
|
||||||
|
</b-progress-bar>
|
||||||
|
</b-progress>
|
||||||
|
|
||||||
|
<h6 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h6>
|
||||||
|
<b-progress height="1.5rem" :max="space.max_users" variant="success" :striped="true">
|
||||||
|
<b-progress-bar :value="space.user_count">
|
||||||
|
{{ space.user_count }} /
|
||||||
|
<template v-if="space.max_users === 0">∞</template>
|
||||||
|
<template v-else>{{ space.max_users }}</template>
|
||||||
|
</b-progress-bar>
|
||||||
|
</b-progress>
|
||||||
|
|
||||||
|
<h6 class="mt-2"><i class="fas fa-file"></i> {{ $t('Files') }}</h6>
|
||||||
|
<b-progress height="1.5rem" :max="space.max_file_storage_mb" variant="success" :striped="true">
|
||||||
|
<b-progress-bar :value="space.file_size_mb">
|
||||||
|
{{ space.file_size_mb }} /
|
||||||
|
<template v-if="space.max_file_storage_mb === 0">∞</template>
|
||||||
|
<template v-else>{{ space.max_file_storage_mb }}</template>
|
||||||
|
</b-progress-bar>
|
||||||
|
</b-progress>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-2">
|
<div class="row mt-4">
|
||||||
<div class="col col-12">
|
<div class="col col-12">
|
||||||
<div v-if="user_spaces !== undefined">
|
<div v-if="user_spaces !== undefined">
|
||||||
|
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h4>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -48,15 +73,15 @@
|
|||||||
|
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col col-12">
|
<div class="col col-12">
|
||||||
<button @click="show_invite_create = true">Create</button>
|
|
||||||
<div v-if="invite_links !== undefined">
|
<div v-if="invite_links !== undefined">
|
||||||
|
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Invites') }}</h4>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>{{ $t('Email') }}</th>
|
<th>{{ $t('Email') }}</th>
|
||||||
<th>{{ $t('Group') }}</th>
|
<th>{{ $t('Group') }}</th>
|
||||||
<th>{{ $t('Token') }}</th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -75,7 +100,6 @@
|
|||||||
:multiple="false"
|
:multiple="false"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td><input class="form-control" disabled v-model="il.uuid"></td>
|
|
||||||
<td><input type="date" v-model="il.valid_until" class="form-control"></td>
|
<td><input type="date" v-model="il.valid_until" class="form-control"></td>
|
||||||
<td>
|
<td>
|
||||||
<b-dropdown no-caret right>
|
<b-dropdown no-caret right>
|
||||||
@ -83,20 +107,20 @@
|
|||||||
<i class="fas fa-ellipsis-v"></i>
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<b-dropdown-item>
|
<!-- <b-dropdown-item>-->
|
||||||
<i class="fas fa-share-alt"></i>
|
<!-- <i class="fas fa-share-alt"></i>-->
|
||||||
|
<!-- </b-dropdown-item>-->
|
||||||
|
|
||||||
|
<b-dropdown-item @click="copyToClipboard(il, true)">
|
||||||
|
<i class="fas fa-link"></i> {{ $t('Copy Link') }}
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
|
|
||||||
<b-dropdown-item>
|
<b-dropdown-item @click="copyToClipboard(il, false)">
|
||||||
<i class="fas fa-link"></i>
|
<i class="far fa-clipboard"></i> {{ $t('Copy Token') }}
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
|
|
||||||
<b-dropdown-item>
|
<b-dropdown-item @click="deleteInviteLink(il)">
|
||||||
<i class="far fa-clipboard"></i>
|
<i class="fas fa-trash-alt"></i> {{ $t('Delete') }}
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item>
|
|
||||||
{{ $t('Delete') }}
|
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
|
|
||||||
|
|
||||||
@ -105,6 +129,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<b-button variant="primary" @click="show_invite_create = true">{{ $t('Create') }}</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,6 +179,13 @@ export default {
|
|||||||
this.loadInviteLinks()
|
this.loadInviteLinks()
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
loadInviteLinks: function () {
|
||||||
let apiFactory = new ApiApiFactory()
|
let apiFactory = new ApiApiFactory()
|
||||||
apiFactory.listInviteLinks().then(r => {
|
apiFactory.listInviteLinks().then(r => {
|
||||||
@ -178,7 +210,17 @@ export default {
|
|||||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -125,6 +125,8 @@
|
|||||||
"Move": "Move",
|
"Move": "Move",
|
||||||
"Merge": "Merge",
|
"Merge": "Merge",
|
||||||
"Parent": "Parent",
|
"Parent": "Parent",
|
||||||
|
"Copy Link": "Copy Link",
|
||||||
|
"Copy Token": "Copy Token",
|
||||||
"delete_confirmation": "Are you sure that you want to delete {source}?",
|
"delete_confirmation": "Are you sure that you want to delete {source}?",
|
||||||
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
|
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
|
||||||
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
|
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
|
||||||
@ -262,6 +264,8 @@
|
|||||||
"New_Cookbook": "New cookbook",
|
"New_Cookbook": "New cookbook",
|
||||||
"Hide_Keyword": "Hide keywords",
|
"Hide_Keyword": "Hide keywords",
|
||||||
"Clear": "Clear",
|
"Clear": "Clear",
|
||||||
|
"Users": "Users",
|
||||||
|
"Invites": "Invites",
|
||||||
"err_move_self": "Cannot move item to itself",
|
"err_move_self": "Cannot move item to itself",
|
||||||
"nothing": "Nothing to do",
|
"nothing": "Nothing to do",
|
||||||
"err_merge_self": "Cannot merge item with itself",
|
"err_merge_self": "Cannot merge item with itself",
|
||||||
|
Reference in New Issue
Block a user