moved space stuff to database and reworked invite link backend logic

This commit is contained in:
vabene1111
2025-09-11 21:44:40 +02:00
parent ad4b1393dd
commit 723b74509f
46 changed files with 213 additions and 48 deletions

View File

@@ -40,6 +40,9 @@ class ScopeMiddleware:
if request.path.startswith(prefix + '/switch-space/'): if request.path.startswith(prefix + '/switch-space/'):
return self.get_response(request) return self.get_response(request)
if request.path.startswith(prefix + '/invite/'):
return self.get_response(request)
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point) # get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
user_space = request.user.userspace_set.filter(active=True).first() user_space = request.user.userspace_set.filter(active=True).first()
@@ -49,6 +52,9 @@ class ScopeMiddleware:
user_space = request.user.userspace_set.filter(active=True).first() user_space = request.user.userspace_set.filter(active=True).first()
user_space.active = True user_space.active = True
user_space.save() user_space.save()
elif 'signup_token' in request.session:
# if user is authenticated, has no space but a signup token (InviteLink) is present, redirect to invite link logic
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
else: else:
# if user does not yet have a space create one for him # if user does not yet have a space create one for him
user_space = create_space_for_user(request.user) user_space = create_space_for_user(request.user)

View File

@@ -405,6 +405,11 @@ class SpaceSerializer(WritableNestedModelSerializer):
return 0 return 0
def create(self, validated_data): def create(self, validated_data):
if Space.objects.filter(created_by=self.context['request'].user).count() >= self.context['request'].user.userpreference.max_owned_spaces:
raise serializers.ValidationError(
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({self.context['request'].user.userpreference.max_owned_spaces})')
name = None name = None
if 'name' in validated_data: if 'name' in validated_data:
name = validated_data['name'] name = validated_data['name']

View File

@@ -78,10 +78,11 @@ urlpatterns = [
path('setup/', views.setup, name='view_setup'), path('setup/', views.setup, name='view_setup'),
path('no-group/', views.no_groups, name='view_no_group'), path('no-group/', views.no_groups, name='view_no_group'),
path('space-overview/', views.space_overview, name='view_space_overview'), #path('space-overview/', views.space_overview, name='view_space_overview'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'), #path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm/', views.no_perm, name='view_no_perm'), #path('no-perm/', views.no_perm, name='view_no_perm'),
path('invite/<slug:token>', views.invite_link, name='view_invite'), path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('invite/<slug:token>/', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'), path('system/', views.system, name='view_system'),
path('plugin/update/', views.plugin_update, name='view_plugin_update'), path('plugin/update/', views.plugin_update, name='view_plugin_update'),

View File

@@ -181,7 +181,10 @@ class StandardFilterModelViewSet(viewsets.ModelViewSet):
queryset = self.queryset queryset = self.queryset
query = self.request.query_params.get('query', None) query = self.request.query_params.get('query', None)
if query is not None: if query is not None:
queryset = queryset.filter(name__icontains=query) try:
queryset = queryset.filter(name__icontains=query)
except FieldError:
pass
updated_at = self.request.query_params.get('updated_at', None) updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None: if updated_at is not None:
@@ -1892,8 +1895,8 @@ class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
if internal_note is not None: if internal_note is not None:
self.queryset = self.queryset.filter(internal_note=internal_note) self.queryset = self.queryset.filter(internal_note=internal_note)
unused = self.request.query_params.get('unused', False) used = self.request.query_params.get('used', False)
if unused: if not used:
self.queryset = self.queryset.filter(used_by=None) self.queryset = self.queryset.filter(used_by=None)
if is_space_owner(self.request.user, self.request.space): if is_space_owner(self.request.user, self.request.space):

View File

@@ -42,6 +42,9 @@ def index(request, path=None, resource=None):
if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS: if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
return HttpResponseRedirect(reverse_lazy('view_setup')) return HttpResponseRedirect(reverse_lazy('view_setup'))
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'): if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'):
return render(request, 'frontend/tandoor.html', {}) return render(request, 'frontend/tandoor.html', {})
else: else:
@@ -98,7 +101,7 @@ def space_overview(request):
max_users=settings.SPACE_DEFAULT_MAX_USERS, max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING, allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
ai_enabled=settings.SPACE_AI_ENABLED, ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,) ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY, )
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False) user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get()) user_space.groups.add(Group.objects.filter(name='admin').get())
@@ -322,7 +325,7 @@ def invite_link(request, token):
try: try:
token = UUID(token, version=4) token = UUID(token, version=4)
except ValueError: except ValueError:
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!')) print('Malformed Invite Link supplied!')
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first(): if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
@@ -331,22 +334,17 @@ def invite_link(request, token):
link.used_by = request.user link.used_by = request.user
link.save() link.save()
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False) UserSpace.objects.filter(user=request.user).update(active=False)
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=True)
if request.user.userspace_set.count() == 1:
user_space.active = True
user_space.save()
user_space.groups.add(link.group) user_space.groups.add(link.group)
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.')) return HttpResponseRedirect(reverse('index'))
return HttpResponseRedirect(reverse('view_space_overview'))
else: else:
request.session['signup_token'] = str(token) request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup')) return HttpResponseRedirect(reverse('account_signup'))
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!')) return HttpResponseRedirect(reverse('index'))
return HttpResponseRedirect(reverse('view_space_overview'))
def report_share_abuse(request, token): def report_share_abuse(request, token):

View File

@@ -156,13 +156,16 @@ const router = useRouter()
const isPrintMode = useMediaQuery('print') const isPrintMode = useMediaQuery('print')
onMounted(() => { onMounted(() => {
useUserPreferenceStore() useUserPreferenceStore().init()
}) })
/** /**
* global title update handler, might be overridden by page specific handlers * global title update handler, might be overridden by page specific handlers
*/ */
router.afterEach((to, from) => { router.afterEach((to, from) => {
if(to.name != 'WelcomePage' && !useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!){
router.push({name: 'WelcomePage'})
}
nextTick(() => { nextTick(() => {
if (to.meta.title) { if (to.meta.title) {
title.value = t(to.meta.title) title.value = t(to.meta.title)

View File

@@ -0,0 +1,76 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import {ConnectorConfig, Space} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
const props = defineProps({
item: {type: {} as PropType<Space>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<Space>, required: false, default: {} as Space},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {
setupState,
deleteObject,
saveObject,
isUpdate,
editingObjName,
loading,
editingObj,
editingObjChanged,
modelClass
} = useModelEditorFunctions<Space>('Space', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor(){
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}
</script>
<style scoped>
</style>

View File

@@ -120,6 +120,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "", "GroupBy": "",
"Hide_Food": "", "Hide_Food": "",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -117,6 +117,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Групирай по", "GroupBy": "Групирай по",
"Hide_Food": "Скриване на храна", "Hide_Food": "Скриване на храна",
"Hide_Keyword": "Скриване на ключови думи", "Hide_Keyword": "Скриване на ключови думи",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupat per", "GroupBy": "Agrupat per",
"Hide_Food": "Amagar Aliment", "Hide_Food": "Amagar Aliment",
"Hide_Keyword": "Amaga les paraules clau", "Hide_Keyword": "Amaga les paraules clau",

View File

@@ -160,6 +160,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Seskupit podle", "GroupBy": "Seskupit podle",
"Hide_Food": "Skrýt potravinu", "Hide_Food": "Skrýt potravinu",
"Hide_Keyword": "Skrýt štítky", "Hide_Keyword": "Skrýt štítky",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Grupper efter", "GroupBy": "Grupper efter",
"Hide_Food": "Skjul mad", "Hide_Food": "Skjul mad",
"Hide_Keyword": "Skjul nøgleord", "Hide_Keyword": "Skjul nøgleord",

View File

@@ -224,6 +224,7 @@
"GettingStarted": "Erste Schritte", "GettingStarted": "Erste Schritte",
"Global": "Global", "Global": "Global",
"GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.", "GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.",
"Group": "Gruppe",
"GroupBy": "Gruppieren nach", "GroupBy": "Gruppieren nach",
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht", "HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
"Headline": "Überschrift", "Headline": "Überschrift",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Ομαδοποίηση κατά", "GroupBy": "Ομαδοποίηση κατά",
"Hide_Food": "Απόκρυψη φαγητού", "Hide_Food": "Απόκρυψη φαγητού",
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί", "Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Getting Started", "GettingStarted": "Getting Started",
"Global": "Global", "Global": "Global",
"GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ", "GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ",
"Group": "Group",
"GroupBy": "Group By", "GroupBy": "Group By",
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food", "HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
"Headline": "Headline", "Headline": "Headline",

View File

@@ -215,6 +215,7 @@
"GettingStarted": "Primeros pasos", "GettingStarted": "Primeros pasos",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por", "GroupBy": "Agrupar por",
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento", "HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
"Headline": "Encabezado", "Headline": "Encabezado",

View File

@@ -158,6 +158,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Ryhmittely peruste", "GroupBy": "Ryhmittely peruste",
"Hide_Food": "Piilota Ruoka", "Hide_Food": "Piilota Ruoka",
"Hide_Keyword": "Piilota avainsana", "Hide_Keyword": "Piilota avainsana",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Commencer", "GettingStarted": "Commencer",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Grouper par", "GroupBy": "Grouper par",
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment", "HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
"Headline": "En-tête", "Headline": "En-tête",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "אסוף לפי", "GroupBy": "אסוף לפי",
"Hide_Food": "הסתר אוכל", "Hide_Food": "הסתר אוכל",
"Hide_Keyword": "הסתר מילות מפתח", "Hide_Keyword": "הסתר מילות מפתח",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Grupiraj po", "GroupBy": "Grupiraj po",
"Hide_Food": "Sakrij namirnicu", "Hide_Food": "Sakrij namirnicu",
"Hide_Keyword": "Sakrij ključne riječi", "Hide_Keyword": "Sakrij ključne riječi",

View File

@@ -144,6 +144,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Csoportosítva", "GroupBy": "Csoportosítva",
"Hide_Food": "Alapanyag elrejtése", "Hide_Food": "Alapanyag elrejtése",
"Hide_Keyword": "Kulcsszavak elrejtése", "Hide_Keyword": "Kulcsszavak elrejtése",

View File

@@ -69,6 +69,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"Hide_Food": "Թաքցնել սննդամթերքը", "Hide_Food": "Թաքցնել սննդամթերքը",
"Hide_Keywords": "Թաքցնել բանալի բառը", "Hide_Keywords": "Թաքցնել բանալի բառը",
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը", "Hide_Recipes": "Թաքցնել բաղադրատոմսերը",

View File

@@ -132,6 +132,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "", "GroupBy": "",
"Hide_Food": "", "Hide_Food": "",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -160,6 +160,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "", "GroupBy": "",
"Hide_Food": "", "Hide_Food": "",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Iniziamo", "GettingStarted": "Iniziamo",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Raggruppa per", "GroupBy": "Raggruppa per",
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento", "HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
"Headline": "Intestazione", "Headline": "Intestazione",

View File

@@ -146,6 +146,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "", "GroupBy": "",
"Hide_Food": "", "Hide_Food": "",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "", "GroupBy": "",
"Hide_Food": "", "Hide_Food": "",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -152,6 +152,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Grupér", "GroupBy": "Grupér",
"Hide_Food": "Skjul Matrett", "Hide_Food": "Skjul Matrett",
"Hide_Keyword": "Skjul nøkkelord", "Hide_Keyword": "Skjul nøkkelord",

View File

@@ -223,6 +223,7 @@
"GettingStarted": "Aan de slag", "GettingStarted": "Aan de slag",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Groepeer per", "GroupBy": "Groepeer per",
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel", "HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
"Headline": "Koptekst", "Headline": "Koptekst",

View File

@@ -187,6 +187,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Grupuj według", "GroupBy": "Grupuj według",
"Hide_Food": "Ukryj żywność", "Hide_Food": "Ukryj żywność",
"Hide_Keyword": "Ukryj słowa kluczowe", "Hide_Keyword": "Ukryj słowa kluczowe",

View File

@@ -132,6 +132,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por", "GroupBy": "Agrupar por",
"Hide_Food": "Esconder comida", "Hide_Food": "Esconder comida",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -221,6 +221,7 @@
"GettingStarted": "Começando", "GettingStarted": "Começando",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar Por", "GroupBy": "Agrupar Por",
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento", "HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
"Headline": "Título", "Headline": "Título",

View File

@@ -139,6 +139,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Grupat de", "GroupBy": "Grupat de",
"Hide_Food": "Ascunde mâncare", "Hide_Food": "Ascunde mâncare",
"Hide_Keyword": "Ascunde cuvintele cheie", "Hide_Keyword": "Ascunde cuvintele cheie",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Начало работы", "GettingStarted": "Начало работы",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Сгруппировать по", "GroupBy": "Сгруппировать по",
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.", "HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
"Headline": "Заголовок", "Headline": "Заголовок",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Začetek", "GettingStarted": "Začetek",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Združi po", "GroupBy": "Združi po",
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano", "HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
"Headline": "Glavni naslov", "Headline": "Glavni naslov",

View File

@@ -198,6 +198,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Gruppera enligt", "GroupBy": "Gruppera enligt",
"Hide_Food": "Dölj livsmedel", "Hide_Food": "Dölj livsmedel",
"Hide_Keyword": "Dölj nyckelord", "Hide_Keyword": "Dölj nyckelord",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "Gruplandırma Ölçütü", "GroupBy": "Gruplandırma Ölçütü",
"Hide_Food": "Yiyeceği Gizle", "Hide_Food": "Yiyeceği Gizle",
"Hide_Keyword": "Anahtar kelimeleri gizle", "Hide_Keyword": "Anahtar kelimeleri gizle",

View File

@@ -142,6 +142,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "По Групі", "GroupBy": "По Групі",
"Hide_Food": "Сховати Їжу", "Hide_Food": "Сховати Їжу",
"Hide_Keyword": "", "Hide_Keyword": "",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "", "FuzzySearchHelp": "",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "分组", "GroupBy": "分组",
"Hide_Food": "隐藏食物", "Hide_Food": "隐藏食物",
"Hide_Keyword": "隐藏关键词", "Hide_Keyword": "隐藏关键词",

View File

@@ -221,6 +221,7 @@
"GettingStarted": "開始使用", "GettingStarted": "開始使用",
"Global": "", "Global": "",
"GlobalHelp": "", "GlobalHelp": "",
"Group": "",
"GroupBy": "分組依據", "GroupBy": "分組依據",
"HeaderWarning": "警告:變更為標題會刪除數量/單位/食物", "HeaderWarning": "警告:變更為標題會刪除數量/單位/食物",
"Headline": "標題", "Headline": "標題",

View File

@@ -35,6 +35,17 @@
<database-model-col model="MealType"></database-model-col> <database-model-col model="MealType"></database-model-col>
</v-row> </v-row>
<v-row>
<v-col>
<h2>{{ $t('Space') }}</h2>
</v-col>
</v-row>
<v-row dense>
<database-model-col model="Space"></database-model-col>
<database-model-col model="UserSpace"></database-model-col>
<database-model-col model="InviteLink"></database-model-col>
</v-row>
<template v-if="useUserPreferenceStore().activeSpace.aiEnabled"> <template v-if="useUserPreferenceStore().activeSpace.aiEnabled">
<v-row> <v-row>
<v-col> <v-col>

View File

@@ -43,7 +43,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col> <v-col>
<v-text-field prepend-inner-icon="$search" :label="$t('Search')" v-model="query" clearable></v-text-field> <v-text-field prepend-inner-icon="$search" :label="$t('Search')" v-model="query" v-if="!genericModel.model.disableSearch" clearable></v-text-field>
<v-data-table-server <v-data-table-server
v-model="selectedItems" v-model="selectedItems"
@@ -82,13 +82,16 @@
<v-chip label v-if="item.space == null" color="success">{{ $t('Global') }}</v-chip> <v-chip label v-if="item.space == null" color="success">{{ $t('Global') }}</v-chip>
<v-chip label v-else color="info">{{ $t('Space') }}</v-chip> <v-chip label v-else color="info">{{ $t('Space') }}</v-chip>
</template> </template>
<template v-slot:item.groups="{ item }" v-if="genericModel.model.name == 'UserSpace'">
{{item.groups.flatMap((x: Group) => x.name).join(', ')}}
</template>
<template v-slot:item.action="{ item }"> <template v-slot:item.action="{ item }">
<v-btn class="float-right" icon="$menu" variant="plain"> <v-btn class="float-right" icon="$menu" variant="plain">
<v-icon icon="$menu"></v-icon> <v-icon icon="$menu"></v-icon>
<v-menu activator="parent" close-on-content-click> <v-menu activator="parent" close-on-content-click>
<v-list density="compact"> <v-list density="compact">
<v-list-item prepend-icon="$edit" :to="{name: 'ModelEditPage', params: {model: model, id: item.id}}" <v-list-item prepend-icon="$edit" :to="{name: 'ModelEditPage', params: {model: model, id: item.id}}"
v-if="!genericModel.model.disableCreate && !genericModel.model.disableUpdate && !genericModel.model.disableDelete"> v-if="!(genericModel.model.disableCreate && genericModel.model.disableUpdate && genericModel.model.disableDelete)">
{{ $t('Edit') }} {{ $t('Edit') }}
</v-list-item> </v-list-item>
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" v-if="genericModel.model.isMerge" link> <v-list-item prepend-icon="fa-solid fa-arrows-to-dot" v-if="genericModel.model.isMerge" link>
@@ -144,7 +147,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelMergeDialog from "@/components/dialogs/ModelMergeDialog.vue"; import ModelMergeDialog from "@/components/dialogs/ModelMergeDialog.vue";
import {VDataTableUpdateOptions} from "@/vuetify"; import {VDataTableUpdateOptions} from "@/vuetify";
import SyncDialog from "@/components/dialogs/SyncDialog.vue"; import SyncDialog from "@/components/dialogs/SyncDialog.vue";
import {ApiApi, ApiRecipeListRequest, RecipeImport} from "@/openapi"; import {ApiApi, ApiRecipeListRequest, Group, RecipeImport} from "@/openapi";
import {useTitle} from "@vueuse/core"; import {useTitle} from "@vueuse/core";
import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue"; import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue";
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue"; import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";

View File

@@ -64,8 +64,6 @@ import SearchPage from "@/pages/SearchPage.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
const router = useRouter()
const totalRecipes = ref(-1) const totalRecipes = ref(-1)
onMounted(() => { onMounted(() => {
@@ -74,10 +72,6 @@ onMounted(() => {
api.apiRecipeList({pageSize: 1}).then((r) => { api.apiRecipeList({pageSize: 1}).then((r) => {
totalRecipes.value = r.count totalRecipes.value = r.count
}) })
if (!useUserPreferenceStore().activeSpace.spaceSetupCompleted) {
router.push({name: 'WelcomePage'})
}
}) })
</script> </script>

View File

@@ -17,7 +17,7 @@
<v-stepper-window> <v-stepper-window>
<v-stepper-window-item value="1"> <v-stepper-window-item value="1">
<v-card flat> <v-card flat>
<v-card-title class="text-h4">{{ $t('WelcometoTandoor') }}</v-card-title> <v-card-title class="text-h4">{{ $t('WelcometoTandoor') }} <span class="text-tandoor">{{useUserPreferenceStore().userSettings.user.displayName}}</span></v-card-title>
<v-card-text v-if="space"> <v-card-text v-if="space">
<p class="text-subtitle-1 mb-4">{{ $t('WelcomeSettingsHelp') }}</p> <p class="text-subtitle-1 mb-4">{{ $t('WelcomeSettingsHelp') }}</p>

View File

@@ -67,7 +67,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
function loadUserSettings() { function loadUserSettings() {
console.log('loading user settings from DB') console.log('loading user settings from DB')
let api = new ApiApi() let api = new ApiApi()
api.apiUserPreferenceList().then(r => { return api.apiUserPreferenceList().then(r => {
if (r.length == 1) { if (r.length == 1) {
userSettings.value = r[0] userSettings.value = r[0]
isAuthenticated.value = true isAuthenticated.value = true
@@ -104,7 +104,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/ */
function loadServerSettings() { function loadServerSettings() {
let api = new ApiApi() let api = new ApiApi()
api.apiServerSettingsCurrentRetrieve().then(r => { return api.apiServerSettingsCurrentRetrieve().then(r => {
serverSettings.value = r serverSettings.value = r
}).catch(err => { }).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -116,7 +116,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/ */
function loadActiveSpace() { function loadActiveSpace() {
let api = new ApiApi() let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => { return api.apiSpaceCurrentRetrieve().then(r => {
activeSpace.value = r activeSpace.value = r
}).catch(err => { }).catch(err => {
if (err.response.status != 403) { if (err.response.status != 403) {
@@ -130,7 +130,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/ */
function loadUserSpaces() { function loadUserSpaces() {
let api = new ApiApi() let api = new ApiApi()
api.apiUserSpaceList().then(r => { return api.apiUserSpaceList().then(r => {
userSpaces.value = r.results userSpaces.value = r.results
}).catch(err => { }).catch(err => {
if (err.response.status != 403) { if (err.response.status != 403) {
@@ -146,7 +146,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
function loadSpaces() { function loadSpaces() {
let api = new ApiApi() let api = new ApiApi()
api.apiSpaceList().then(r => { return api.apiSpaceList().then(r => {
spaces.value = r.results spaces.value = r.results
}).catch(err => { }).catch(err => {
if (err.response.status != 403) { if (err.response.status != 403) {
@@ -162,9 +162,10 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
let api = new ApiApi() let api = new ApiApi()
api.apiSwitchActiveSpaceRetrieve({spaceId: space.id!}).then(r => { api.apiSwitchActiveSpaceRetrieve({spaceId: space.id!}).then(r => {
loadActiveSpace() loadActiveSpace().then(() => {
router.push({name: 'StartPage'}).then(() => { router.push({name: 'StartPage'}).then(() => {
location.reload() location.reload()
})
}) })
}).catch(err => { }).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -223,15 +224,20 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
} }
} }
// always load settings on first initialization of store function init() {
loadUserSettings() const promises = [] as Promise<any>[]
loadServerSettings() promises.push(loadUserSettings())
loadActiveSpace() promises.push(loadServerSettings())
loadUserSpaces() promises.push(loadActiveSpace())
loadSpaces() promises.push(loadUserSpaces())
updateTheme() promises.push(loadSpaces())
updateTheme()
return Promise.allSettled(promises)
}
return { return {
init,
deviceSettings, deviceSettings,
userSettings, userSettings,
serverSettings, serverSettings,

View File

@@ -1,13 +1,13 @@
import { import {
AccessToken, AiLog, AiProvider, AccessToken, AiLog, AiProvider,
ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter, ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
Food, Food, FoodInheritField,
Ingredient, Ingredient,
InviteLink, Keyword, InviteLink, Keyword,
MealPlan, MealPlan,
MealType, MealType,
Property, PropertyType, Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry, Space,
Step, Step,
Supermarket, Supermarket,
SupermarketCategory, Sync, SyncLog, SupermarketCategory, Sync, SyncLog,
@@ -101,6 +101,7 @@ export type Model = {
disableCreate?: boolean | undefined, disableCreate?: boolean | undefined,
disableUpdate?: boolean | undefined, disableUpdate?: boolean | undefined,
disableDelete?: boolean | undefined, disableDelete?: boolean | undefined,
disableSearch?: boolean | undefined,
// disable showing this model as an option in the ModelListPage // disable showing this model as an option in the ModelListPage
disableListView?: boolean | undefined, disableListView?: boolean | undefined,
@@ -148,6 +149,8 @@ export type EditorSupportedModels =
| 'SearchFields' | 'SearchFields'
| 'AiProvider' | 'AiProvider'
| 'AiLog' | 'AiLog'
| 'Space'
| 'FoodInheritField'
// used to type methods/parameters in conjunction with configuration type // used to type methods/parameters in conjunction with configuration type
export type EditorSupportedTypes = export type EditorSupportedTypes =
@@ -184,6 +187,8 @@ export type EditorSupportedTypes =
| SearchFields | SearchFields
| AiProvider | AiProvider
| AiLog | AiLog
| Space
| FoodInheritField
export const TFood = { export const TFood = {
name: 'Food', name: 'Food',
@@ -655,7 +660,8 @@ export const TUserSpace = {
disableCreate: true, disableCreate: true,
tableHeaders: [ tableHeaders: [
{title: 'User', key: 'user'}, {title: 'User', key: 'user.displayName'},
{title: 'Group', key: 'groups'},
{title: 'Actions', key: 'action', align: 'end'}, {title: 'Actions', key: 'action', align: 'end'},
] ]
} as Model } as Model
@@ -669,19 +675,39 @@ export const TInviteLink = {
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/InviteLinkEditor.vue`)), editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/InviteLinkEditor.vue`)),
disableListView: true, disableSearch: true,
isPaginated: true, isPaginated: true,
toStringKeys: ['email', 'role'], toStringKeys: ['email', 'role'],
tableHeaders: [ tableHeaders: [
{title: 'Email', key: 'email'}, {title: 'Email', key: 'email'},
{title: 'Role', key: 'group'}, {title: 'Role', key: 'group.name'},
{title: 'Valid Until', key: 'validUntil'}, {title: 'Valid Until', key: 'validUntil'},
{title: 'Actions', key: 'action', align: 'end'}, {title: 'Actions', key: 'action', align: 'end'},
] ]
} as Model } as Model
registerModel(TInviteLink) registerModel(TInviteLink)
export const TSpace = {
name: 'Space',
localizationKey: 'Space',
localizationKeyDescription: 'SpaceHelp',
icon: 'fa-solid fa-hard-drive',
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/SpaceEditor.vue`)),
disableDelete: true,
isPaginated: true,
toStringKeys: ['name'],
tableHeaders: [
{title: 'Name', key: 'name'},
{title: 'Owner', key: 'createdBy.displayName'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TSpace)
export const TStorage = { export const TStorage = {
name: 'Storage', name: 'Storage',
localizationKey: 'Storage', localizationKey: 'Storage',