basic auto sync working

This commit is contained in:
vabene1111 2024-01-04 20:22:31 +01:00
parent 964afd5f73
commit 3cab0ab52e
3 changed files with 147 additions and 69 deletions

View File

@ -30,6 +30,7 @@ from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from icalendar import Calendar, Event
@ -1163,10 +1164,13 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
if 'checked' in self.request.query_params or 'recent' in self.request.query_params:
return shopping_helper(self.queryset, self.request)
last_autosync = self.request.query_params.get('last_autosync', None)
if last_autosync:
last_autosync = datetime.datetime.now() # TODO implement
self.queryset = self.queryset.filter(updated_at__gte=last_autosync)
try:
last_autosync = self.request.query_params.get('last_autosync', None)
if last_autosync:
last_autosync = make_aware(datetime.datetime.fromtimestamp(int(last_autosync) / 1000))
self.queryset = self.queryset.filter(updated_at__gte=last_autosync)
except:
traceback.print_exc()
# TODO once old shopping list is removed this needs updated to sharing users in preferences
return self.queryset

View File

@ -29,9 +29,13 @@
<!-- shopping list tab -->
<b-tab active>
<template #title>
<b-spinner v-if="shopping_list_store.currently_updating" type="border" small class="d-inline-block"></b-spinner>
<i v-if="!shopping_list_store.currently_updating" class="fas fa-shopping-cart fa-fw d-inline-block d-md-none"></i>
<span class="d-none d-md-inline-block">{{ $t('Shopping_list') + ` (${Object.keys(shopping_list_store.entries).length})` }}</span> <!-- TODO properly count only checked -->
<b-spinner v-if="shopping_list_store.currently_updating" type="border" small
class="d-inline-block"></b-spinner>
<i v-if="!shopping_list_store.currently_updating"
class="fas fa-shopping-cart fa-fw d-inline-block d-md-none"></i>
<span class="d-none d-md-inline-block">{{
$t('Shopping_list') + ` (${Object.keys(shopping_list_store.entries).length})`
}}</span> <!-- TODO properly count only checked -->
</template>
<b-row class="d-lg-block d-print-none d-none pr-1 pl-1 mb-3 mt-3">
@ -52,13 +56,17 @@
<!-- shopping list table -->
<b-row v-for="c in shopping_list_store.get_entries_by_group" v-bind:key="c.id" class="pr-1 pl-1">
<b-col cols="12" v-if="c.count_unchecked > 0 || user_preference_store.device_settings.shopping_show_checked_entries && (c.count_unchecked + c.count_ecked) > 0">
<b-col cols="12"
v-if="c.count_unchecked > 0 || user_preference_store.device_settings.shopping_show_checked_entries && (c.count_unchecked + c.count_ecked) > 0">
<b-button-group class="w-100 mt-1">
<b-button variant="light" block class="btn btn-block text-left">
<span v-if="c.name === shopping_list_store.UNDEFINED_CATEGORY">{{ $t('Undefined') }}</span>
<span v-if="c.name === shopping_list_store.UNDEFINED_CATEGORY">{{
$t('Undefined')
}}</span>
<span v-else>{{ c.name }}</span>
</b-button>
<b-button variant="success"><i class="fas fa-check fa-fw"></i></b-button> <!-- todo implement -->
<b-button variant="success"><i class="fas fa-check fa-fw"></i></b-button>
<!-- todo implement -->
</b-button-group>
<span v-for="f in c.foods" v-bind:key="f.id">
@ -72,7 +80,9 @@
<b-tab :title="$t('Recipes')">
<template #title>
<i class="fas fa-book fa-fw d-block d-md-none"></i>
<span class="d-none d-md-block">{{ $t('Recipes') + ` (${Object.keys(shopping_list_store.getAssociatedRecipes()).length})` }}</span>
<span class="d-none d-md-block">{{
$t('Recipes') + ` (${Object.keys(shopping_list_store.getAssociatedRecipes()).length})`
}}</span>
</template>
<b-row class="d-lg-block d-print-none d-none pr-1 pl-1 mb-3 mt-3">
@ -85,23 +95,37 @@
</b-col>
</b-row>
<b-row v-for="r in shopping_list_store.getAssociatedRecipes()" :key="r.shopping_list_recipe_id" class="pr-1 pl-1">
<b-row v-for="r in shopping_list_store.getAssociatedRecipes()" :key="r.shopping_list_recipe_id"
class="pr-1 pl-1">
<b-col cols="12">
<b-button-group class="w-100 mt-2">
<b-button variant="dark" block class="btn btn-block text-left">
<span>{{ r.recipe_name }}</span> <br/>
<span><small class="text-muted">{{ r.recipe_name }}</small></span> <!-- TODO show meal plan date/type -->
<span><small class="text-muted">{{ r.recipe_name }}</small></span>
<!-- TODO show meal plan date/type -->
</b-button>
<!-- <b-form-input min="1" type="number" :debounce="300" v-model="r.servings" @update="updateServings(r.shopping_list_recipe_id, r.servings)"></b-form-input>-->
<b-button variant="danger" @click="deleteRecipe(r.shopping_list_recipe_id)"><i class="fas fa-trash fa-fw"></i></b-button>
<b-button variant="danger" @click="deleteRecipe(r.shopping_list_recipe_id)"><i
class="fas fa-trash fa-fw"></i></b-button>
</b-button-group>
<b-button-group class="w-100 mt-1">
<b-button @click="r.servings = updateServings(r, 'half')" :disabled="shopping_list_store.currently_updating"><i class="fas fa-divide"></i> 2</b-button>
<b-button variant="info" @click="r.servings = updateServings(r, 'sub')" :disabled="shopping_list_store.currently_updating"><i class="fas fa-minus"></i></b-button>
<b-button variant="info" @click="r.servings = updateServings(r, 'prompt')">{{ r.servings }}</b-button>
<b-button variant="info" @click="r.servings = updateServings(r, 'add')" :disabled="shopping_list_store.currently_updating"><i class="fas fa-plus"></i></b-button>
<b-button @click="r.servings = updateServings(r, 'multiply')" :disabled="shopping_list_store.currently_updating"><i class="fas fa-times"></i> 2</b-button>
<b-button @click="r.servings = updateServings(r, 'half')"
:disabled="shopping_list_store.currently_updating"><i class="fas fa-divide"></i> 2
</b-button>
<b-button variant="info" @click="r.servings = updateServings(r, 'sub')"
:disabled="shopping_list_store.currently_updating"><i class="fas fa-minus"></i>
</b-button>
<b-button variant="info" @click="r.servings = updateServings(r, 'prompt')">{{
r.servings
}}
</b-button>
<b-button variant="info" @click="r.servings = updateServings(r, 'add')"
:disabled="shopping_list_store.currently_updating"><i class="fas fa-plus"></i>
</b-button>
<b-button @click="r.servings = updateServings(r, 'multiply')"
:disabled="shopping_list_store.currently_updating"><i class="fas fa-times"></i> 2
</b-button>
</b-button-group>
</b-col>
@ -111,7 +135,9 @@
<b-tab>
<template #title>
<i class="fas fa-store-alt fa-fw d-block d-md-none"></i>
<span class="d-none d-md-block">{{ $t('Supermarkets') + ` (${shopping_list_store.supermarkets.length})` }}</span>
<span class="d-none d-md-block">{{
$t('Supermarkets') + ` (${shopping_list_store.supermarkets.length})`
}}</span>
</template>
<div class="container p-0">
<div class="row">
@ -129,7 +155,8 @@
></span></h5>
<b-list-group>
<b-card no-body class="mt-1 list-group-item p-2"
v-for="(supermarket, index) in shopping_list_store.supermarkets" v-hover
v-for="(supermarket, index) in shopping_list_store.supermarkets"
v-hover
:key="supermarket.id">
<b-card-header class="p-2 border-0">
<b-row>
@ -188,7 +215,8 @@
<div v-if="editingSupermarket.length === 0">
<b-list-group>
<b-card no-body class="mt-1 list-group-item p-2"
v-for="(category, index) in shopping_list_store.supermarket_categories" v-hover
v-for="(category, index) in shopping_list_store.supermarket_categories"
v-hover
:key="category.id">
<b-card-header class="p-2 border-0">
<b-row>
@ -313,7 +341,8 @@
</template>
<div class="row justify-content-center">
<div class="col-12 col-md-8">
<shopping-settings-component @updated="settings = $event" :user_id="user_id"></shopping-settings-component>
<shopping-settings-component @updated="settings = $event"
:user_id="user_id"></shopping-settings-component>
</div>
</div>
</b-tab>
@ -324,37 +353,48 @@
<div>
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
<b-form-select v-model="user_preference_store.device_settings.shopping_selected_grouping" size="sm">
<b-form-select-option v-for="go in shopping_list_store.grouping_options" :value="go.id" v-bind:key="go.id">{{ $t(go.translatable_label) }}</b-form-select-option>
<b-form-select-option v-for="go in shopping_list_store.grouping_options" :value="go.id"
v-bind:key="go.id">{{ $t(go.translatable_label) }}
</b-form-select-option>
</b-form-select>
</b-form-group>
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
<generic-multiselect :model="Models.SUPERMARKET" :initial_single_selection="user_preference_store.device_settings.shopping_selected_supermarket"
@change="user_preference_store.device_settings.shopping_selected_supermarket = $event.val; user_preference_store.updateDeviceSettings()" :multiple="false"></generic-multiselect>
<generic-multiselect :model="Models.SUPERMARKET"
:initial_single_selection="user_preference_store.device_settings.shopping_selected_supermarket"
@change="user_preference_store.device_settings.shopping_selected_supermarket = $event.val; user_preference_store.updateDeviceSettings()"
:multiple="false"></generic-multiselect>
</b-form-group>
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1"
class="mb-1">
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_show_delayed_entries" @change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_show_delayed_entries"
@change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
</b-form-group>
<b-form-group v-bind:label="$t('ShowRecentlyCompleted')" label-for="popover-input-3" content-cols="1"
class="mb-1">
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_show_checked_entries" @change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_show_checked_entries"
@change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
</b-form-group>
<b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-5"
content-cols="1" class="mb-1">
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_show_selected_supermarket_only" @change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
<b-form-checkbox
v-model="user_preference_store.device_settings.shopping_show_selected_supermarket_only"
@change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
</b-form-group>
<span>{{ $t('Information') }}</span>
<b-form-group v-bind:label="$t('Recipe')" label-for="popover-input-5"
content-cols="1" class="mb-1">
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_item_info_recipe" @change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_item_info_recipe"
@change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
</b-form-group>
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-5"
content-cols="1" class="mb-1">
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_item_info_mealplan" @change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_item_info_mealplan"
@change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
</b-form-group>
<b-form-group v-bind:label="$t('created_by')" label-for="popover-input-5"
content-cols="1" class="mb-1">
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_item_info_created_by" @change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
<b-form-checkbox v-model="user_preference_store.device_settings.shopping_item_info_created_by"
@change="user_preference_store.updateDeviceSettings()"></b-form-checkbox>
</b-form-group>
</div>
<div class="row" style="margin-top: 1vh; min-width: 300px">
@ -366,7 +406,8 @@
</div>
</b-popover>
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :modal_id="new_recipe.id" :servings="new_recipe.servings" :mealplan="undefined" @finish="finishShopping"/>
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :modal_id="new_recipe.id"
:servings="new_recipe.servings" :mealplan="undefined" @finish="finishShopping"/>
<bottom-navigation-bar active-view="view_shopping">
<template #custom_nav_content v-if="current_tab <= 1">
@ -375,7 +416,8 @@
<template v-if="current_tab===0">
<b-input-group>
<b-form-input v-model="new_item.ingredient" :placeholder="$t('Food')" @keyup.enter="addItem"></b-form-input>
<b-form-input v-model="new_item.ingredient" :placeholder="$t('Food')"
@keyup.enter="addItem"></b-form-input>
<b-input-group-append>
<b-button @click="addItem" variant="success">
<i class="fas fa-cart-plus "/>
@ -535,45 +577,33 @@ export default {
}
},
},
watch: {
"settings.shopping_auto_sync": function (newVal, oldVal) {
clearInterval(this.autosync_id)
this.autosync_id = undefined
if (this.settings.shopping_auto_sync > 0) {
if (!newVal) {
window.removeEventListener("online", this.updateOnlineStatus)
window.removeEventListener("offline", this.updateOnlineStatus)
return
} else if (oldVal === 0 && newVal > 0) {
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
}
this.autosync_id = setInterval(() => {
if (this.online && !this.auto_sync_running) {
this.auto_sync_running = true
this.getShoppingList(true)
}
}, this.settings.shopping_auto_sync * 1000)
}
},
},
watch: {},
mounted() {
//this.getShoppingList()
if (this.settings.shopping_auto_sync) {
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
}
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
this.$i18n.locale = window.CUSTOM_LOCALE
this.shopping_list_store.refreshFromAPI()
this.setupAutoSync()
},
methods: {
useShoppingListStore,
setupAutoSync: function () {
// prevent setting up multiple loops on accident
// TODO should this just raise an error?
clearInterval(this.autosync_id)
this.autosync_id = undefined
let timeout = Math.max(this.settings.shopping_auto_sync, 1) * 1000 // if disabled (shopping_auto_sync=0) check again after 1 second if enabled
this.autosync_id = setInterval(() => { //TODO does setInterval automatically loop (because it kind of did)
if (this.online && this.settings.shopping_auto_sync > 0) {
this.shopping_list_store.autosync()
this.setupAutoSync()
}
}, timeout)
},
/**
* failed requests to sync entry check events are automatically re-queued by the service worker for sync
* this command allows to manually force replaying those events before re-enabling automatic sync
@ -589,6 +619,7 @@ export default {
* get the number of entries left in the sync queue for entry check events
* @returns {Promise<Number>} promise resolving to the number of entries left
*/
//TODO maybe show this somewhere if efficient enough to run often
getSyncQueueLength: function () {
const wb = new Workbox('/service-worker.js');
wb.register();
@ -740,14 +771,17 @@ export default {
if (recipe.servings > 0 && recipe.servings !== "") {
let api = new ApiApiFactory()
api.partialUpdateShoppingListRecipe(recipe.shopping_list_recipe_id, {id: recipe.shopping_list_recipe_id, servings: recipe.servings}).then(() => {
api.partialUpdateShoppingListRecipe(recipe.shopping_list_recipe_id, {
id: recipe.shopping_list_recipe_id,
servings: recipe.servings
}).then(() => {
useShoppingListStore().refreshFromAPI()
})
return recipe.servings
}
},
// TODO cleanup, review data structure, probably move to its own component --> FOR ALL SUPERMARKET FUNCTIONS
// TODO cleanup, review data structure, probably move to its own component --> FOR ALL SUPERMARKET FUNCTIONS
deleteSupermarket(index) {
this.$bvModal.msgBoxConfirm(this.$t('Are_You_Sure'), {
title: this.$t('Confirm'),

View File

@ -4,6 +4,7 @@ import {defineStore} from "pinia"
import Vue from "vue"
import _ from 'lodash';
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import moment from "moment/moment";
const _STORE_ID = "shopping_list_store"
/*
@ -19,6 +20,7 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
// internal
currently_updating: false,
last_autosync: null,
// constants
GROUP_CATEGORY: 'food.supermarket_category.name',
@ -117,7 +119,10 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
* @return {[{id: *, translatable_label: string},{id: *, translatable_label: string},{id: *, translatable_label: string}]}
*/
grouping_options: function () {
return [{'id': this.GROUP_CATEGORY, 'translatable_label': 'Category'}, {'id': this.GROUP_CREATED_BY, 'translatable_label': 'created_by'}, {
return [{'id': this.GROUP_CATEGORY, 'translatable_label': 'Category'}, {
'id': this.GROUP_CREATED_BY,
'translatable_label': 'created_by'
}, {
'id': this.GROUP_RECIPE,
'translatable_label': 'Recipe'
}]
@ -139,6 +144,7 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
})
this.currently_updating = false
}).catch((err) => {
this.currently_updating = false
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
@ -155,6 +161,32 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
})
}
},
autosync() {
if (!this.currently_updating) {
console.log('running autosync')
this.currently_updating = true
let previous_autosync = this.last_autosync
this.last_autosync = new Date().getTime();
let apiClient = new ApiApiFactory()
apiClient.listShoppingListEntrys(undefined, undefined, undefined, {
'query': {'last_autosync': previous_autosync}
}).then((r) => {
r.data.forEach((e) => {
// dont update stale client data
if (Date.parse(this.entries[e.id].updated_at) <= Date.parse(e.updated_at)) { //TODO validate the django datetime can be parsed in all browsers
console.log('updating entry ', e)
Vue.set(this.entries, e.id, e)
}
})
this.currently_updating = false
}).catch((err) => {
console.log('auto sync failed')
this.currently_updating = false
})
}
},
/**
* Create a new shopping list entry
* adds new entry to store
@ -180,6 +212,10 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
*/
updateObject(object) {
let apiClient = new ApiApiFactory()
// set the update_at timestamp on the client to prevent auto sync from overriding with older changes
// moment().format() yields locale aware datetime without ms 2024-01-04T13:39:08.607238+01:00
Vue.set(object, 'update_at', moment().format())
console.log('set local update timestamp to ', moment().format())
return apiClient.updateShoppingListEntry(object.id, object).then((r) => {
Vue.set(this.entries, r.data.id, r.data)
}).catch((err) => {
@ -238,7 +274,11 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
Vue.set(structure, grouping_key, {'name': grouping_key, 'foods': {}})
}
if (!(entry.food.id in structure[grouping_key]['foods'])) {
Vue.set(structure[grouping_key]['foods'], entry.food.id, {'id': entry.food.id, 'name': entry.food.name, 'entries': {}})
Vue.set(structure[grouping_key]['foods'], entry.food.id, {
'id': entry.food.id,
'name': entry.food.name,
'entries': {}
})
}
Vue.set(structure[grouping_key]['foods'][entry.food.id]['entries'], entry.id, entry)
return structure