settings wip

This commit is contained in:
vabene1111 2022-07-15 17:12:01 +02:00
parent 8700e2df69
commit ce1f55ffd1
23 changed files with 801 additions and 90 deletions

View File

@ -62,7 +62,7 @@ class UserPreferenceAdmin(admin.ModelAdmin):
@staticmethod
def name(obj):
return obj.user.get_user_name()
return obj.user.get_user_display_name()
admin.site.register(UserPreference, UserPreferenceAdmin)
@ -75,7 +75,7 @@ class SearchPreferenceAdmin(admin.ModelAdmin):
@staticmethod
def name(obj):
return obj.user.get_user_name()
return obj.user.get_user_display_name()
admin.site.register(SearchPreference, SearchPreferenceAdmin)
@ -177,7 +177,7 @@ class RecipeAdmin(admin.ModelAdmin):
@staticmethod
def created_by(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
actions = [rebuild_index]
@ -216,7 +216,7 @@ class CommentAdmin(admin.ModelAdmin):
@staticmethod
def name(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
admin.site.register(Comment, CommentAdmin)
@ -235,7 +235,7 @@ class RecipeBookAdmin(admin.ModelAdmin):
@staticmethod
def user_name(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
admin.site.register(RecipeBook, RecipeBookAdmin)
@ -253,7 +253,7 @@ class MealPlanAdmin(admin.ModelAdmin):
@staticmethod
def user(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
admin.site.register(MealPlan, MealPlanAdmin)

View File

@ -320,6 +320,24 @@ class CustomRecipePermission(permissions.BasePermission):
return has_group_permission(request.user, ['guest']) and obj.space == request.space
class CustomUserPermission(permissions.BasePermission):
"""
Custom permission class for user api endpoint
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view): # a space filtered user list is visible for everyone
return has_group_permission(request.user, ['guest'])
def has_object_permission(self, request, view, obj): # object write permissions are only available for user
if request.method in SAFE_METHODS and 'pk' in view.kwargs and has_group_permission(request.user, ['guest']) and request.space in obj.userspace_set.all():
return True
elif request.user == obj:
return True
else:
return False
def above_space_limit(space): # TODO add file storage limit
"""
Test if the space has reached any limit (e.g. max recipes, users, ..)

View File

@ -43,7 +43,7 @@ class Integration:
self.export_type = export_type
self.ignored_recipes = []
description = f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
description = f'Imported by {request.user.get_user_display_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
icon = '📥'
try:

View File

@ -26,7 +26,7 @@ from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PR
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
def get_user_name(self):
def get_user_display_name(self):
if not (name := f"{self.first_name} {self.last_name}") == " ":
return name
else:
@ -58,7 +58,7 @@ def get_shopping_share(self):
]))
auth.models.User.add_to_class('get_user_name', get_user_name)
auth.models.User.add_to_class('get_user_display_name', get_user_display_name)
auth.models.User.add_to_class('get_shopping_share', get_shopping_share)
auth.models.User.add_to_class('get_active_space', get_active_space)

View File

@ -133,16 +133,17 @@ class SpaceFilterSerializer(serializers.ListSerializer):
return super().to_representation(data)
class UserNameSerializer(WritableNestedModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
class UserSerializer(WritableNestedModelSerializer):
display_name = serializers.SerializerMethodField('get_user_label')
def get_user_label(self, obj):
return obj.get_user_name()
return obj.get_user_display_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username')
fields = ('id', 'username', 'first_name', 'last_name', 'display_name')
read_only_fields = ('username', )
class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
@ -279,7 +280,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
class UserSpaceSerializer(WritableNestedModelSerializer):
user = UserNameSerializer(read_only=True)
user = UserSerializer(read_only=True)
groups = GroupSerializer(many=True)
def validate(self, data):
@ -317,8 +318,8 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
class UserPreferenceSerializer(WritableNestedModelSerializer):
food_inherit_default = serializers.SerializerMethodField('get_food_inherit_defaults')
plan_share = UserNameSerializer(many=True, allow_null=True, required=False)
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
plan_share = UserSerializer(many=True, allow_null=True, required=False)
shopping_share = UserSerializer(many=True, allow_null=True, required=False)
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
image = UserFileViewSerializer(required=False, allow_null=True, many=False)
@ -737,7 +738,7 @@ class RecipeSerializer(RecipeBaseSerializer):
keywords = KeywordSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
shared = UserNameSerializer(many=True, required=False)
shared = UserSerializer(many=True, required=False)
class Meta:
model = Recipe
@ -783,7 +784,7 @@ class CommentSerializer(serializers.ModelSerializer):
class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserNameSerializer(many=True, required=False)
shared = UserSerializer(many=True, required=False)
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
@ -796,7 +797,7 @@ class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerialize
class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserNameSerializer(many=True)
shared = UserSerializer(many=True)
filter = CustomFilterSerializer(allow_null=True, required=False)
def create(self, validated_data):
@ -840,7 +841,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
note_markdown = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()
shared = UserNameSerializer(many=True, required=False, allow_null=True)
shared = UserSerializer(many=True, required=False, allow_null=True)
shopping = serializers.SerializerMethodField('in_shopping')
def get_note_markdown(self, obj):
@ -904,7 +905,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserNameSerializer(read_only=True)
created_by = UserSerializer(read_only=True)
completed_at = serializers.DateTimeField(allow_null=True, required=False)
def get_fields(self, *args, **kwargs):
@ -972,7 +973,7 @@ class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
class ShoppingListSerializer(WritableNestedModelSerializer):
recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
entries = ShoppingListEntrySerializer(many=True, allow_null=True)
shared = UserNameSerializer(many=True)
shared = UserSerializer(many=True)
supermarket = SupermarketSerializer(allow_null=True)
def create(self, validated_data):
@ -1085,7 +1086,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
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 = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.get_user_display_name())
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'

View File

@ -285,7 +285,7 @@
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_space,view_settings,view_history,view_system,docs_markdown' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i
class="fas fa-fw fa-user-alt"></i> {{ user.get_user_name }}
class="fas fa-fw fa-user-alt"></i> {{ user.get_user_display_name }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">

View File

@ -7,22 +7,11 @@ from django.urls import reverse
from cookbook.models import UserSpace
LIST_URL = 'api:username-list'
DETAIL_URL = 'api:username-detail'
LIST_URL = 'api:user-list'
DETAIL_URL = 'api:user-detail'
def test_forbidden_methods(u1_s1):
r = u1_s1.post(
reverse(LIST_URL))
assert r.status_code == 405
r = u1_s1.put(
reverse(
DETAIL_URL,
args=[auth.get_user(u1_s1).pk])
)
assert r.status_code == 405
r = u1_s1.delete(
reverse(
DETAIL_URL,
@ -69,3 +58,56 @@ def test_list_space(u1_s1, u2_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 403],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_user_retrieve(arg, request, u1_s1):
c = request.getfixturevalue(arg[0])
r = c.get(reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), )
print(r.content, auth.get_user(u1_s1).username)
assert r.status_code == arg[1]
def test_user_update(u1_s1, u2_s1,u1_s2):
# can update own user
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={auth.get_user(u1_s1).id}
),
{'first_name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 200
assert response['first_name'] == 'test'
# can't update another user
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={auth.get_user(u2_s1).id}
),
{'first_name': 'test'},
content_type='application/json'
)
assert r.status_code == 403
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={auth.get_user(u1_s2).id}
),
{'first_name': 'test'},
content_type='application/json'
)
assert r.status_code == 404

View File

@ -47,7 +47,7 @@ router.register(r'sync', api.SyncViewSet)
router.register(r'sync-log', api.SyncLogViewSet)
router.register(r'unit', api.UnitViewSet)
router.register(r'user-file', api.UserFileViewSet)
router.register(r'user-name', api.UserNameViewSet, basename='username')
router.register(r'user', api.UserViewSet)
router.register(r'user-preference', api.UserPreferenceViewSet)
router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)

View File

@ -53,7 +53,7 @@ from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
CustomIsOwnerReadOnly, CustomIsShare, CustomIsShared,
CustomIsSpaceOwner, CustomIsUser, group_required,
is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission)
is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission, CustomUserPermission)
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup
from cookbook.helper.scrapers.scrapers import text_scraper
@ -85,7 +85,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SupermarketCategoryRelationSerializer,
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
@ -354,7 +354,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
return Response(content, status=status.HTTP_400_BAD_REQUEST)
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
class UserViewSet(viewsets.ModelViewSet):
"""
list:
optional parameters
@ -362,9 +362,9 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
- **filter_list**: array of user id's to get names for
"""
queryset = User.objects
serializer_class = UserNameSerializer
permission_classes = [CustomIsGuest]
http_method_names = ['get']
serializer_class = UserSerializer
permission_classes = [CustomUserPermission]
http_method_names = ['get', 'patch']
def get_queryset(self):
queryset = self.queryset.filter(userspace__space=self.request.space)

View File

@ -96,7 +96,7 @@ def space_overview(request):
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
create_form = SpaceCreateForm(initial={'name': f'{request.user.username}\'s Space'})
create_form = SpaceCreateForm(initial={'name': f'{request.user.get_user_display_name()}\'s Space'})
join_form = SpaceJoinForm()
return render(request, 'space_overview.html', {'create_form': create_form, 'join_form': join_form})

View File

@ -205,7 +205,7 @@
@change="recipe.shared = $event.val"
parent_variable="recipe.shared"
:initial_selection="recipe.shared"
:label="'username'"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"

View File

@ -23,8 +23,18 @@
</b-nav>
</div>
<div class="col-md-9 col-12">
Overview
<cosmetic-settings-component v-if="visible_settings === 'cosmetic'" :user_id="user_id"></cosmetic-settings-component>
<cosmetic-settings-component v-if="visible_settings === 'cosmetic'"
:user_id="user_id"></cosmetic-settings-component>
<account-settings-component v-if="visible_settings === 'account'"
:user_id="user_id"></account-settings-component>
<search-settings-component v-if="visible_settings === 'search'"
:user_id="user_id"></search-settings-component>
<shopping-settings-component v-if="visible_settings === 'shopping'"
:user_id="user_id"></shopping-settings-component>
<meal-plan-settings-component v-if="visible_settings === 'meal_plan'"
:user_id="user_id"></meal-plan-settings-component>
<a-p-i-settings-component v-if="visible_settings === 'api'" :user_id="user_id"></a-p-i-settings-component>
</div>
</div>
</template>
@ -34,18 +44,26 @@ import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api"
import CookbookSlider from "@/components/CookbookSlider"
import LoadingSpinner from "@/components/LoadingSpinner"
import {StandardToasts, ApiMixin} from "@/utils/utils"
import CosmeticSettingsComponent from "@/components/Settings/CosmeticSettingsComponent";
import AccountSettingsComponent from "@/components/Settings/AccountSettingsComponent";
import SearchSettingsComponent from "@/components/Settings/SearchSettingsComponent";
import ShoppingSettingsComponent from "@/components/Settings/ShoppingSettingsComponent";
import MealPlanSettingsComponent from "@/components/Settings/MealPlanSettingsComponent";
import APISettingsComponent from "@/components/Settings/APISettingsComponent";
Vue.use(BootstrapVue)
export default {
name: "ProfileView",
mixins: [],
components: {CosmeticSettingsComponent},
components: {
CosmeticSettingsComponent,
AccountSettingsComponent,
SearchSettingsComponent,
ShoppingSettingsComponent,
MealPlanSettingsComponent,
APISettingsComponent
},
data() {
return {
visible_settings: 'cosmetic',
@ -55,9 +73,7 @@ export default {
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {
},
methods: {},
}
</script>

View File

@ -514,7 +514,7 @@
"
:model="Models.USER"
:initial_selection="settings.shopping_share"
label="username"
label="display_name"
:multiple="true"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('User')"
@ -868,7 +868,7 @@ export default {
case "category":
return item?.food?.supermarket_category?.name ?? x
case "created_by":
return item?.created_by?.username ?? x
return item?.created_by?.display_name ?? x
case "recipe":
return item?.recipe_mealplan?.recipe_name ?? x
}

View File

@ -5,7 +5,7 @@
{{ book_copy.icon }}&nbsp;{{ book_copy.name }}
<span class="float-right text-primary" @click="editOrSave"><i class="fa" v-bind:class="{ 'fa-pen': !editing, 'fa-save': editing }" aria-hidden="true"></i></span>
</h5>
<b-badge class="font-weight-normal mr-1" v-for="u in book_copy.shared" v-bind:key="u.id" variant="primary" pill>{{ u.username }}</b-badge>
<b-badge class="font-weight-normal mr-1" v-for="u in book_copy.shared" v-bind:key="u.id" variant="primary" pill>{{ u.display_name }}</b-badge>
</b-card-header>
<b-card-body class="p-4">
<div class="form-group" v-if="editing">
@ -25,7 +25,7 @@
@change="book_copy.shared = $event.val"
parent_variable="book.shared"
:initial_selection="book.shared"
:label="'username'"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"

View File

@ -64,7 +64,7 @@
required
@change="entryEditing.shared = $event.val"
parent_variable="entryEditing.shared"
:label="'username'"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"

View File

@ -0,0 +1,59 @@
<template>
<div v-if="user_preferences !== undefined">
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "APISettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,78 @@
<template>
<div v-if="user !== undefined">
<b-form-input v-model="user.username" @change="updateUser(false)" disabled></b-form-input>
<b-form-input v-model="user.first_name" @change="updateUser(false)"></b-form-input>
<b-form-input v-model="user.last_name" @change="updateUser(false)"></b-form-input>
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "AccountSettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
user: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
apiFactory.retrieveUser(this.user_id.toString()).then(result => {
this.user = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateUser: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUser(this.user_id.toString(), this.user).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<div v-if="user_preferences !== undefined">
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "MealPlanSettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<div v-if="user_preferences !== undefined">
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "SearchSettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<div v-if="user_preferences !== undefined">
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "ShoppingSettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -385,7 +385,7 @@ export default {
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
},
formatOneCreatedBy: function (item) {
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
return [this.$t("Added_by"), item?.created_by.display_name, "@", this.formatDate(item.created_at)].join(" ")
},
openRecipeCard: function (e, item) {
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, {id: item.recipe_mealplan.recipe}).then((result) => {

View File

@ -591,7 +591,7 @@ export class Models {
type: "lookup",
field: "shared",
list: "USER",
list_label: "username",
list_label: "display_name",
label: "shared_with",
multiple: true,
},

View File

@ -279,11 +279,29 @@ export interface CustomFilterShared {
*/
id?: number;
/**
*
* Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
* @type {string}
* @memberof CustomFilterShared
*/
username?: string;
/**
*
* @type {string}
* @memberof CustomFilterShared
*/
first_name?: string;
/**
*
* @type {string}
* @memberof CustomFilterShared
*/
last_name?: string;
/**
*
* @type {string}
* @memberof CustomFilterShared
*/
display_name?: string;
}
/**
*
@ -2592,11 +2610,29 @@ export interface ShoppingListCreatedBy {
*/
id?: number;
/**
*
* Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
* @type {string}
* @memberof ShoppingListCreatedBy
*/
username?: string;
/**
*
* @type {string}
* @memberof ShoppingListCreatedBy
*/
first_name?: string;
/**
*
* @type {string}
* @memberof ShoppingListCreatedBy
*/
last_name?: string;
/**
*
* @type {string}
* @memberof ShoppingListCreatedBy
*/
display_name?: string;
}
/**
*
@ -3491,6 +3527,43 @@ export interface Unit {
*/
description?: string | null;
}
/**
*
* @export
* @interface User
*/
export interface User {
/**
*
* @type {number}
* @memberof User
*/
id?: number;
/**
* Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
* @type {string}
* @memberof User
*/
username?: string;
/**
*
* @type {string}
* @memberof User
*/
first_name?: string;
/**
*
* @type {string}
* @memberof User
*/
last_name?: string;
/**
*
* @type {string}
* @memberof User
*/
display_name?: string;
}
/**
*
* @export
@ -3534,25 +3607,6 @@ export interface UserFile {
*/
file_size_kb?: number;
}
/**
*
* @export
* @interface UserName
*/
export interface UserName {
/**
*
* @type {number}
* @memberof UserName
*/
id?: number;
/**
*
* @type {string}
* @memberof UserName
*/
username?: string;
}
/**
*
* @export
@ -4664,6 +4718,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createUser: async (user?: User, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/user/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} name
@ -5646,6 +5733,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
destroyUser: async (id: string, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('destroyUser', 'id', id)
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -7045,7 +7165,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
* @throws {RequiredError}
*/
listUsers: async (options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/user-name/`;
const localVarPath = `/api/user/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -8303,6 +8423,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateUser: async (id: string, user?: User, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('partialUpdateUser', 'id', id)
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -9484,7 +9641,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
retrieveUser: async (id: string, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('retrieveUser', 'id', id)
const localVarPath = `/api/user-name/{id}/`
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -10705,6 +10862,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateUser: async (id: string, user?: User, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateUser', 'id', id)
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -11073,6 +11267,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createUnit(unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createUser(user?: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(user, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} name
@ -11368,6 +11572,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroyUnit(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async destroyUser(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroyUser(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -11765,7 +11979,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listUsers(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserName>>> {
async listUsers(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<User>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUsers(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -12135,6 +12349,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateUnit(id, unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async partialUpdateUser(id: string, user?: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateUser(id, user, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -12490,7 +12715,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async retrieveUser(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserName>> {
async retrieveUser(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveUser(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -12851,6 +13076,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateUnit(id, unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateUser(id: string, user?: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(id, user, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -13113,6 +13349,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
createUnit(unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.createUnit(unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createUser(user?: User, options?: any): AxiosPromise<User> {
return localVarFp.createUser(user, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} name
@ -13379,6 +13624,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
destroyUnit(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroyUnit(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
destroyUser(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroyUser(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -13740,7 +13994,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listUsers(options?: any): AxiosPromise<Array<UserName>> {
listUsers(options?: any): AxiosPromise<Array<User>> {
return localVarFp.listUsers(options).then((request) => request(axios, basePath));
},
/**
@ -14076,6 +14330,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
partialUpdateUnit(id: string, unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.partialUpdateUnit(id, unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateUser(id: string, user?: User, options?: any): AxiosPromise<User> {
return localVarFp.partialUpdateUser(id, user, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -14397,7 +14661,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveUser(id: string, options?: any): AxiosPromise<UserName> {
retrieveUser(id: string, options?: any): AxiosPromise<User> {
return localVarFp.retrieveUser(id, options).then((request) => request(axios, basePath));
},
/**
@ -14724,6 +14988,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
updateUnit(id: string, unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.updateUnit(id, unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateUser(id: string, user?: User, options?: any): AxiosPromise<User> {
return localVarFp.updateUser(id, user, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -15034,6 +15308,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).createUnit(unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public createUser(user?: User, options?: any) {
return ApiApiFp(this.configuration).createUser(user, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} name
@ -15358,6 +15643,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).destroyUnit(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public destroyUser(id: string, options?: any) {
return ApiApiFp(this.configuration).destroyUser(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -16195,6 +16491,18 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).partialUpdateUnit(id, unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public partialUpdateUser(id: string, user?: User, options?: any) {
return ApiApiFp(this.configuration).partialUpdateUser(id, user, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user file.
@ -16979,6 +17287,18 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).updateUnit(id, unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public updateUser(id: string, user?: User, options?: any) {
return ApiApiFp(this.configuration).updateUser(id, user, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user file.