mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-24 02:39:20 -05:00
migrated comments, improved recipe activity, added editor
This commit is contained in:
34
cookbook/migrations/0223_auto_20250831_1111.py
Normal file
34
cookbook/migrations/0223_auto_20250831_1111.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 4.2.22 on 2025-08-31 09:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_comments(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
|
Comment = apps.get_model('cookbook', 'Comment')
|
||||||
|
CookLog = apps.get_model('cookbook', 'CookLog')
|
||||||
|
|
||||||
|
cook_logs = []
|
||||||
|
|
||||||
|
for c in Comment.objects.all():
|
||||||
|
cook_logs.append(CookLog(
|
||||||
|
recipe=c.recipe,
|
||||||
|
created_by=c.created_by,
|
||||||
|
created_at=c.created_at,
|
||||||
|
comment=c.text,
|
||||||
|
space=c.recipe.space,
|
||||||
|
))
|
||||||
|
|
||||||
|
CookLog.objects.bulk_create(cook_logs, unique_fields=('recipe', 'comment', 'created_at', 'created_by'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0222_alter_shoppinglistrecipe_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_comments),
|
||||||
|
]
|
||||||
@@ -1,40 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<v-card class="mt-1 d-print-none" v-if="useUserPreferenceStore().isAuthenticated" :loading="loading">
|
||||||
<v-card class="mt-1" v-if="cookLogs.length > 0">
|
|
||||||
<v-card-title>{{ $t('Activity') }}</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item v-for="c in cookLogs" :key="c.id">
|
|
||||||
<template #prepend>
|
|
||||||
<v-avatar color="primary">V</v-avatar>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title class="font-weight-bold">{{ c.createdBy.displayName }}
|
|
||||||
<v-rating density="comfortable" size="x-small" color="tandoor" class="float-right" v-model="c.rating" half-increments readonly v-if="c.rating != undefined"></v-rating>
|
|
||||||
</v-list-item-title>
|
|
||||||
|
|
||||||
{{ c.comment }}
|
|
||||||
|
|
||||||
<p v-if="c.servings != null && c.servings > 0">
|
|
||||||
{{ c.servings }}
|
|
||||||
<span v-if="recipe.servingsText != ''">{{ recipe.servingsText }}</span>
|
|
||||||
<span v-else-if="c.servings == 1">{{ $t('Serving') }}</span>
|
|
||||||
<span v-else>{{ $t('Servings') }}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-disabled">
|
|
||||||
{{ DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_SHORT) }}
|
|
||||||
</p>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-card class="mt-1 d-print-none" v-if="useUserPreferenceStore().isAuthenticated">
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-textarea :label="$t('Comment')" rows="2" v-model="newCookLog.comment"></v-textarea>
|
<v-textarea :label="$t('Comment')" rows="2" v-model="newCookLog.comment"></v-textarea>
|
||||||
<v-row de>
|
<v-row dense>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
<v-label>{{$t('Rating')}}</v-label><br/>
|
<v-label>{{ $t('Rating') }}</v-label>
|
||||||
|
<br/>
|
||||||
<v-rating v-model="newCookLog.rating" clearable hover density="compact"></v-rating>
|
<v-rating v-model="newCookLog.rating" clearable hover density="compact"></v-rating>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
@@ -52,6 +23,48 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<v-card class="mt-1" v-if="cookLogs.length > 0" :loading="loading">
|
||||||
|
<v-card-title>{{ $t('Activity') }}</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item class="border-t-sm" v-for="c in cookLogs" :key="c.id" :link="c.createdBy.id == useUserPreferenceStore().userSettings?.user.id">
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar color="primary">{{ c.createdBy.displayName.charAt(0) }}</v-avatar>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
{{ c.createdBy.displayName }}
|
||||||
|
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ c.comment }}</v-list-item-subtitle>
|
||||||
|
|
||||||
|
<v-list-item-subtitle class="font-italic mt-1" v-if="c.servings != null && c.servings > 0">
|
||||||
|
|
||||||
|
{{ c.servings }}
|
||||||
|
<span v-if="recipe.servingsText != ''">{{ recipe.servingsText }}</span>
|
||||||
|
<span v-else-if="c.servings == 1">{{ $t('Serving') }}</span>
|
||||||
|
<span v-else>{{ $t('Servings') }}</span>
|
||||||
|
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<v-list-item-action class="flex-column align-end">
|
||||||
|
<v-rating density="comfortable" size="x-small" color="tandoor" v-model="c.rating" half-increments readonly
|
||||||
|
v-if="c.rating != undefined"></v-rating>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-tooltip location="top" :text="DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_MED)" v-if="c.createdAt != undefined">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<span v-bind="props">{{ DateTime.fromJSDate(c.createdAt).toRelative({style: 'narrow'}) }}</span>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
</v-list-item-action>
|
||||||
|
</template>
|
||||||
|
<model-edit-dialog model="CookLog" :item="c" v-if="c.createdBy.id == useUserPreferenceStore().userSettings?.user.id" @save="recLoadCookLog(props.recipe.id)" @delete="recLoadCookLog(props.recipe.id)"></model-edit-dialog>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,6 +76,7 @@ import {DateTime} from "luxon";
|
|||||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||||
import {VDateInput} from 'vuetify/labs/VDateInput'
|
import {VDateInput} from 'vuetify/labs/VDateInput'
|
||||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||||
|
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
recipe: {
|
recipe: {
|
||||||
@@ -74,21 +88,31 @@ const props = defineProps({
|
|||||||
const newCookLog = ref({} as CookLog);
|
const newCookLog = ref({} as CookLog);
|
||||||
|
|
||||||
const cookLogs = ref([] as CookLog[])
|
const cookLogs = ref([] as CookLog[])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshActivity()
|
recLoadCookLog(props.recipe.id)
|
||||||
resetForm()
|
resetForm()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load cook logs from database for given recipe
|
* recursively load cook logs from database for given recipe
|
||||||
*/
|
*/
|
||||||
function refreshActivity() {
|
function recLoadCookLog(recipeId: number, page: number = 1) {
|
||||||
const api = new ApiApi()
|
const api = new ApiApi()
|
||||||
api.apiCookLogList({recipe: props.recipe.id}).then(r => {
|
loading.value = true
|
||||||
// TODO pagination
|
if(page == 1){
|
||||||
|
cookLogs.value = []
|
||||||
|
}
|
||||||
|
api.apiCookLogList({recipe: props.recipe.id, page: page}).then(r => {
|
||||||
if (r.results) {
|
if (r.results) {
|
||||||
cookLogs.value = r.results.sort((a,b) => a.createdAt! > b.createdAt! ? 1 : -1)
|
cookLogs.value = cookLogs.value.concat(r.results)
|
||||||
|
if (r.next) {
|
||||||
|
recLoadCookLog(recipeId, page + 1)
|
||||||
|
} else {
|
||||||
|
cookLogs.value = cookLogs.value.sort((a, b) => a.createdAt! > b.createdAt! ? 1 : -1)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
81
vue3/src/components/model_editors/CookLogEditor.vue
Normal file
81
vue3/src/components/model_editors/CookLogEditor.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<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-textarea :label="$t('Comment')" rows="2" v-model="editingObj.comment"></v-textarea>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-label>{{ $t('Rating') }}</v-label>
|
||||||
|
<br/>
|
||||||
|
<v-rating v-model="editingObj.rating" clearable hover density="compact"></v-rating>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
|
||||||
|
<v-number-input :label="$t('Servings')" v-model="editingObj.servings" :precision="2"></v-number-input>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-date-input :label="$t('Date')" v-model="editingObj.createdAt"></v-date-input>
|
||||||
|
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</model-editor-base>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {onMounted, PropType, watch} from "vue";
|
||||||
|
import {CookLog} from "@/openapi";
|
||||||
|
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||||
|
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {type: {} as PropType<CookLog>, required: false, default: null},
|
||||||
|
itemId: {type: [Number, String], required: false, default: undefined},
|
||||||
|
itemDefaults: {type: {} as PropType<CookLog>, required: false, default: {} as CookLog},
|
||||||
|
dialog: {type: Boolean, default: false}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||||
|
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<CookLog>('CookLog', 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>
|
||||||
@@ -581,10 +581,11 @@ export const TCookLog = {
|
|||||||
localizationKeyDescription: 'CookLogHelp',
|
localizationKeyDescription: 'CookLogHelp',
|
||||||
icon: 'fa-solid fa-table-list',
|
icon: 'fa-solid fa-table-list',
|
||||||
|
|
||||||
isPaginated: true,
|
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/CookLogEditor.vue`)),
|
||||||
|
|
||||||
disableCreate: true,
|
disableCreate: true,
|
||||||
disableUpdate: true,
|
|
||||||
disableDelete: true,
|
isPaginated: true,
|
||||||
toStringKeys: ['recipe'],
|
toStringKeys: ['recipe'],
|
||||||
|
|
||||||
tableHeaders: [
|
tableHeaders: [
|
||||||
|
|||||||
Reference in New Issue
Block a user