basics of new settings page working
This commit is contained in:
parent
f4df84b609
commit
8700e2df69
19
cookbook/migrations/0182_userpreference_image.py
Normal file
19
cookbook/migrations/0182_userpreference_image.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.0.6 on 2022-07-14 13:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0181_space_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_image', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
]
|
@ -358,22 +358,15 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, related_name='user_image')
|
||||||
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
||||||
nav_color = models.CharField(
|
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||||
choices=COLORS, max_length=128, default=PRIMARY
|
|
||||||
)
|
|
||||||
default_unit = models.CharField(max_length=32, default='g')
|
default_unit = models.CharField(max_length=32, default='g')
|
||||||
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
|
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
|
||||||
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
|
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
|
||||||
default_page = models.CharField(
|
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
|
||||||
choices=PAGES, max_length=64, default=SEARCH
|
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
|
||||||
)
|
shopping_share = models.ManyToManyField(User, blank=True, related_name='shopping_share')
|
||||||
plan_share = models.ManyToManyField(
|
|
||||||
User, blank=True, related_name='plan_share_default'
|
|
||||||
)
|
|
||||||
shopping_share = models.ManyToManyField(
|
|
||||||
User, blank=True, related_name='shopping_share'
|
|
||||||
)
|
|
||||||
ingredient_decimals = models.IntegerField(default=2)
|
ingredient_decimals = models.IntegerField(default=2)
|
||||||
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
|
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
|
||||||
shopping_auto_sync = models.IntegerField(default=5)
|
shopping_auto_sync = models.IntegerField(default=5)
|
||||||
|
@ -320,6 +320,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
|||||||
plan_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
plan_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||||
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||||
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
||||||
|
image = UserFileViewSerializer(required=False, allow_null=True, many=False)
|
||||||
|
|
||||||
def get_food_inherit_defaults(self, obj):
|
def get_food_inherit_defaults(self, obj):
|
||||||
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
|
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
|
||||||
@ -338,8 +339,8 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = UserPreference
|
model = UserPreference
|
||||||
fields = (
|
fields = (
|
||||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
|
'user', 'image', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
|
||||||
'plan_share',
|
'plan_share', 'sticky_navbar',
|
||||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
|
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
|
||||||
'food_inherit_default', 'default_delay',
|
'food_inherit_default', 'default_delay',
|
||||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
|
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
|
||||||
|
45
cookbook/templates/user_settings.html
Normal file
45
cookbook/templates/user_settings.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load render_bundle from webpack_loader %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% load custom_tags %}
|
||||||
|
|
||||||
|
{% block title %}{% trans 'Settings' %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<settings-view></settings-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{% if debug %}
|
||||||
|
<script src="{% url 'js_reverse' %}"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||||
|
window.USER_ID = {{ request.user.pk }}
|
||||||
|
|
||||||
|
<!--TODO build custom API endpoint for this -->
|
||||||
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
|
window.AVAILABLE_LANGUAGES = [
|
||||||
|
{% for language in languages %}
|
||||||
|
['{{ language.name_local }}', '{{ language.code }}'],
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% render_bundle 'settings_view' %}
|
||||||
|
{% endblock %}
|
@ -68,6 +68,7 @@ urlpatterns = [
|
|||||||
path('plan/', views.meal_plan, name='view_plan'),
|
path('plan/', views.meal_plan, name='view_plan'),
|
||||||
path('shopping/', lists.shopping_list, name='view_shopping'),
|
path('shopping/', lists.shopping_list, name='view_shopping'),
|
||||||
path('settings/', views.user_settings, name='view_settings'),
|
path('settings/', views.user_settings, name='view_settings'),
|
||||||
|
path('user-settings/', views.user_settings_new, name='view_user_settings'),
|
||||||
path('history/', views.history, name='view_history'),
|
path('history/', views.history, name='view_history'),
|
||||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||||
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
||||||
|
@ -182,6 +182,11 @@ def view_profile(request, user_id):
|
|||||||
return render(request, 'profile.html', {})
|
return render(request, 'profile.html', {})
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('guest')
|
||||||
|
def user_settings_new(request):
|
||||||
|
return render(request, 'user_settings.html', {})
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def ingredient_editor(request):
|
def ingredient_editor(request):
|
||||||
template_vars = {'food_id': -1, 'unit_id': -1}
|
template_vars = {'food_id': -1, 'unit_id': -1}
|
||||||
|
66
vue/src/apps/SettingsView/SettingsView.vue
Normal file
66
vue/src/apps/SettingsView/SettingsView.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" class="row">
|
||||||
|
<div class="col-md-3 col-12">
|
||||||
|
<b-nav vertical>
|
||||||
|
<b-nav-item :active="visible_settings === 'cosmetic'" @click="visible_settings = 'cosmetic'"><i
|
||||||
|
class="fas fa-fw fa-eye"></i> Cosmetic
|
||||||
|
</b-nav-item>
|
||||||
|
<b-nav-item :active="visible_settings === 'account'" @click="visible_settings = 'account'"><i
|
||||||
|
class="fas fa-fw fa-user"></i> Account
|
||||||
|
</b-nav-item>
|
||||||
|
<b-nav-item :active="visible_settings === 'search'" @click="visible_settings = 'search'"><i
|
||||||
|
class="fas fa-fw fa-search"></i> Search
|
||||||
|
</b-nav-item>
|
||||||
|
<b-nav-item :active="visible_settings === 'shopping'" @click="visible_settings = 'shopping'"><i
|
||||||
|
class="fas fa-fw fa-shopping-cart"></i> Shopping
|
||||||
|
</b-nav-item>
|
||||||
|
<b-nav-item :active="visible_settings === 'meal_plan'" @click="visible_settings = 'meal_plan'"><i
|
||||||
|
class="fas fa-fw fa-calendar"></i> Meal Plan
|
||||||
|
</b-nav-item>
|
||||||
|
<b-nav-item :active="visible_settings === 'api'" @click="visible_settings = 'api'"><i
|
||||||
|
class="fas fa-fw fa-code"></i> API
|
||||||
|
</b-nav-item>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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";
|
||||||
|
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ProfileView",
|
||||||
|
mixins: [],
|
||||||
|
components: {CosmeticSettingsComponent},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible_settings: 'cosmetic',
|
||||||
|
user_id: window.USER_ID,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
17
vue/src/apps/SettingsView/main.js
Normal file
17
vue/src/apps/SettingsView/main.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './SettingsView.vue'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||||
|
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
publicPath = 'http://localhost:8080/'
|
||||||
|
}
|
||||||
|
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
i18n,
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
110
vue/src/components/Settings/CosmeticSettingsComponent.vue
Normal file
110
vue/src/components/Settings/CosmeticSettingsComponent.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="user_preferences !== undefined">
|
||||||
|
|
||||||
|
<b-form-input v-model="user_preferences.default_unit" @change="updateSettings"></b-form-input>
|
||||||
|
|
||||||
|
{{ user_preferences.ingredient_decimals }}
|
||||||
|
<b-form-input type="range" min="0" max="4" step="1" v-model="user_preferences.ingredient_decimals"
|
||||||
|
@change="updateSettings"></b-form-input>
|
||||||
|
|
||||||
|
<b-form-checkbox v-model="user_preferences.use_fractions" @change="updateSettings"></b-form-checkbox>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
Language
|
||||||
|
<b-form-select v-model="$i18n.locale" @change="updateLanguage">
|
||||||
|
<b-form-select-option v-bind:key="l[0]" v-for="l in languages" :value="l[1]">{{ l[0] }} ({{
|
||||||
|
l[1]
|
||||||
|
}})
|
||||||
|
</b-form-select-option>
|
||||||
|
</b-form-select>
|
||||||
|
|
||||||
|
<b-form-select v-model="user_preferences.theme" @change="updateSettings(true);">
|
||||||
|
<b-form-select-option value="TANDOOR">Tandoor</b-form-select-option>
|
||||||
|
<b-form-select-option value="BOOTSTRAP">Bootstrap</b-form-select-option>
|
||||||
|
<b-form-select-option value="DARKLY">Darkly</b-form-select-option>
|
||||||
|
<b-form-select-option value="FLATLY">Flatly</b-form-select-option>
|
||||||
|
<b-form-select-option value="SUPERHERO">Superhero</b-form-select-option>
|
||||||
|
</b-form-select>
|
||||||
|
|
||||||
|
<b-form-checkbox v-model="user_preferences.sticky_navbar" @change="updateSettings(true);"></b-form-checkbox>
|
||||||
|
|
||||||
|
<b-form-select v-model="user_preferences.nav_color" @change="updateSettings(true);">
|
||||||
|
<b-form-select-option value="PRIMARY">Primary</b-form-select-option>
|
||||||
|
<b-form-select-option value="SECONDARY">Secondary</b-form-select-option>
|
||||||
|
<b-form-select-option value="SUCCESS">Success</b-form-select-option>
|
||||||
|
<b-form-select-option value="INFO">Info</b-form-select-option>
|
||||||
|
<b-form-select-option value="WARNING">Warning</b-form-select-option>
|
||||||
|
<b-form-select-option value="DANGER">Danger</b-form-select-option>
|
||||||
|
<b-form-select-option value="LIGHT">Light</b-form-select-option>
|
||||||
|
<b-form-select-option value="DARK">Dark</b-form-select-option>
|
||||||
|
</b-form-select>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<b-form-checkbox v-model="user_preferences.use_kj" @change="updateSettings();"></b-form-checkbox>
|
||||||
|
<b-form-checkbox v-model="user_preferences.comments" @change="updateSettings();"></b-form-checkbox>
|
||||||
|
<b-form-checkbox v-model="user_preferences.left_handed" @change="updateSettings();"></b-form-checkbox>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||||
|
import {resolveDjangoUrl, StandardToasts} from "@/utils/utils";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||||
|
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CosmeticSettingsComponent",
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateLanguage: function () {
|
||||||
|
axios.post(resolveDjangoUrl('set_language'), new URLSearchParams({'language': this.$i18n.locale})).then(result => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||||
|
location.reload()
|
||||||
|
}).catch(err => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -3110,6 +3110,43 @@ export interface Space {
|
|||||||
* @memberof Space
|
* @memberof Space
|
||||||
*/
|
*/
|
||||||
file_size_mb?: string;
|
file_size_mb?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {SpaceImage}
|
||||||
|
* @memberof Space
|
||||||
|
*/
|
||||||
|
image?: SpaceImage;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SpaceImage
|
||||||
|
*/
|
||||||
|
export interface SpaceImage {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof SpaceImage
|
||||||
|
*/
|
||||||
|
id?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SpaceImage
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SpaceImage
|
||||||
|
*/
|
||||||
|
file_download?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SpaceImage
|
||||||
|
*/
|
||||||
|
preview?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -3528,6 +3565,12 @@ export interface UserPreference {
|
|||||||
* @memberof UserPreference
|
* @memberof UserPreference
|
||||||
*/
|
*/
|
||||||
user: number;
|
user: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {RecipeFile}
|
||||||
|
* @memberof UserPreference
|
||||||
|
*/
|
||||||
|
image?: RecipeFile | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -3570,6 +3613,12 @@ export interface UserPreference {
|
|||||||
* @memberof UserPreference
|
* @memberof UserPreference
|
||||||
*/
|
*/
|
||||||
plan_share?: Array<CustomFilterShared> | null;
|
plan_share?: Array<CustomFilterShared> | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof UserPreference
|
||||||
|
*/
|
||||||
|
sticky_navbar?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
@ -64,6 +64,10 @@ const pages = {
|
|||||||
profile_view: {
|
profile_view: {
|
||||||
entry: "./src/apps/ProfileView/main.js",
|
entry: "./src/apps/ProfileView/main.js",
|
||||||
chunks: ["chunk-vendors"],
|
chunks: ["chunk-vendors"],
|
||||||
|
},
|
||||||
|
settings_view: {
|
||||||
|
entry: "./src/apps/SettingsView/main.js",
|
||||||
|
chunks: ["chunk-vendors"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user