download as CSV
This commit is contained in:
parent
7ad088d953
commit
df54b10610
@ -36,7 +36,10 @@ class UserPreferenceForm(forms.ModelForm):
|
|||||||
prefix = 'preference'
|
prefix = 'preference'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
space = kwargs.pop('space')
|
if x := kwargs.get('instance', None):
|
||||||
|
space = x.space
|
||||||
|
else:
|
||||||
|
space = kwargs.pop('space')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all()
|
self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all()
|
||||||
|
|
||||||
@ -483,7 +486,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
fields = (
|
fields = (
|
||||||
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
|
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
|
||||||
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days'
|
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim'
|
||||||
)
|
)
|
||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
@ -498,6 +501,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
'default_delay': _('Default number of hours to delay a shopping list entry.'),
|
'default_delay': _('Default number of hours to delay a shopping list entry.'),
|
||||||
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
|
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
|
||||||
'shopping_recent_days': _('Days of recent shopping list entries to display.'),
|
'shopping_recent_days': _('Days of recent shopping list entries to display.'),
|
||||||
|
'csv_delim': _('Delimiter to use for CSV exports.'),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'shopping_share': _('Share Shopping List'),
|
'shopping_share': _('Share Shopping List'),
|
||||||
@ -507,7 +511,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
'mealplan_autoinclude_related': _('Include Related'),
|
'mealplan_autoinclude_related': _('Include Related'),
|
||||||
'default_delay': _('Default Delay Hours'),
|
'default_delay': _('Default Delay Hours'),
|
||||||
'filter_to_supermarket': _('Filter to Supermarket'),
|
'filter_to_supermarket': _('Filter to Supermarket'),
|
||||||
'shopping_recent_days': _('Recent Days')
|
'shopping_recent_days': _('Recent Days'),
|
||||||
|
'csv_delim': _('CSV Delimiter'),
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
|
18
cookbook/migrations/0162_userpreference_csv_delim.py
Normal file
18
cookbook/migrations/0162_userpreference_csv_delim.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.9 on 2021-11-30 22:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0161_alter_shoppinglistentry_food'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='csv_delim',
|
||||||
|
field=models.CharField(default=',', max_length=2),
|
||||||
|
),
|
||||||
|
]
|
@ -333,6 +333,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
filter_to_supermarket = models.BooleanField(default=False)
|
filter_to_supermarket = models.BooleanField(default=False)
|
||||||
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
||||||
shopping_recent_days = models.PositiveIntegerField(default=7)
|
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||||
|
csv_delim = models.CharField(max_length=2, default=",")
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||||
|
@ -155,7 +155,7 @@ class FoodInheritFieldSerializer(UniqueFieldsMixin):
|
|||||||
class UserPreferenceSerializer(serializers.ModelSerializer):
|
class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||||
# food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True)
|
# food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True)
|
||||||
food_ignore_default = serializers.SerializerMethodField('get_ignore_default')
|
food_ignore_default = serializers.SerializerMethodField('get_ignore_default')
|
||||||
plan_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
|
||||||
|
|
||||||
def get_ignore_default(self, obj):
|
def get_ignore_default(self, obj):
|
||||||
return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data
|
return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data
|
||||||
@ -165,14 +165,19 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# don't allow writing to FoodInheritField via API
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserPreference
|
model = UserPreference
|
||||||
fields = (
|
fields = (
|
||||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj',
|
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj',
|
||||||
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
||||||
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay',
|
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_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', 'csv_delim'
|
||||||
)
|
)
|
||||||
|
read_only_fields = ['user']
|
||||||
|
|
||||||
|
|
||||||
class UserFileSerializer(serializers.ModelSerializer):
|
class UserFileSerializer(serializers.ModelSerializer):
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-muted px-0" id="downloadShoppingLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-muted px-0" id="downloadShoppingLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
||||||
|
|
||||||
<div class="dropdown-menu dropdown-menu-center" aria-labelledby="downloadShoppingLink">
|
<div class="dropdown-menu dropdown-menu-center" aria-labelledby="downloadShoppingLink">
|
||||||
<a class="dropdown-item" @click="download('CSV')"><i class="fas fa-file-import"></i> {{ $t("Download csv") }}</a>
|
<a class="dropdown-item disabled" @click="download('CSV')"><i class="fas fa-file-import"></i> {{ $t("Download csv") }}</a>
|
||||||
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||||
<a class="dropdown-item" @click="download('clipboard')"><i class="fas fa-plus"></i> {{ $t("copy to clipboard") }}</a>
|
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
||||||
<a class="dropdown-item" @click="download('markdown')"><i class="fas fa-plus"></i> {{ $t("copy as markdown") }}</a>
|
<a class="dropdown-item disabled" @click="download('clipboard')"><i class="fas fa-plus"></i> {{ $t("copy to clipboard") }}</a>
|
||||||
|
<a class="dropdown-item disabled" @click="download('markdown')"><i class="fas fa-plus"></i> {{ $t("copy as markdown") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="link" id="id_filters_button" class="px-0">
|
<b-button variant="link" id="id_filters_button" class="px-0">
|
||||||
@ -346,6 +347,19 @@
|
|||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">{{ $t("csv_delim_label") }}</div>
|
||||||
|
<div class="col col-md-6 text-right">
|
||||||
|
<input type="string" size="sm" v-model="settings.csv_delim" @change="saveSettings" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row sm mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<em class="small text-muted">
|
||||||
|
{{ $t("csv_delim_help") }}
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -451,6 +465,7 @@ import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
|||||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||||
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
||||||
import DownloadPDF from "@/components/Buttons/DownloadPDF"
|
import DownloadPDF from "@/components/Buttons/DownloadPDF"
|
||||||
|
import DownloadCSV from "@/components/Buttons/DownloadCSV"
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import GenericPill from "@/components/GenericPill"
|
import GenericPill from "@/components/GenericPill"
|
||||||
import LookupInput from "@/components/Modals/LookupInput"
|
import LookupInput from "@/components/Modals/LookupInput"
|
||||||
@ -465,7 +480,7 @@ Vue.use(BootstrapVue)
|
|||||||
export default {
|
export default {
|
||||||
name: "ShoppingListView",
|
name: "ShoppingListView",
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput, DownloadPDF },
|
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput, DownloadPDF, DownloadCSV },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -488,6 +503,7 @@ export default {
|
|||||||
mealplan_autoexclude_onhand: true,
|
mealplan_autoexclude_onhand: true,
|
||||||
filter_to_supermarket: false,
|
filter_to_supermarket: false,
|
||||||
shopping_recent_days: 7,
|
shopping_recent_days: 7,
|
||||||
|
csv_delim: ",",
|
||||||
},
|
},
|
||||||
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
||||||
new_category: { entrymode: false, value: undefined },
|
new_category: { entrymode: false, value: undefined },
|
||||||
@ -570,6 +586,11 @@ export default {
|
|||||||
})
|
})
|
||||||
return groups
|
return groups
|
||||||
},
|
},
|
||||||
|
csvData() {
|
||||||
|
return this.items.map((x) => {
|
||||||
|
return { amount: x.amount, unit: x.unit?.name ?? "", food: x.food?.name ?? "" }
|
||||||
|
})
|
||||||
|
},
|
||||||
defaultDelay() {
|
defaultDelay() {
|
||||||
return Number(getUserPreference("default_delay")) || 2
|
return Number(getUserPreference("default_delay")) || 2
|
||||||
},
|
},
|
||||||
@ -595,7 +616,6 @@ export default {
|
|||||||
supermarketCategory() {
|
supermarketCategory() {
|
||||||
return this.new_supermarket.editmode ? this.new_supermarket.value.category_to_supermarket : this.shopping_categories
|
return this.new_supermarket.editmode ? this.new_supermarket.value.category_to_supermarket : this.shopping_categories
|
||||||
},
|
},
|
||||||
|
|
||||||
notSupermarketCategory() {
|
notSupermarketCategory() {
|
||||||
let supercats = this.new_supermarket.value.category_to_supermarket
|
let supercats = this.new_supermarket.value.category_to_supermarket
|
||||||
.map((x) => x.category)
|
.map((x) => x.category)
|
||||||
@ -645,6 +665,7 @@ export default {
|
|||||||
|
|
||||||
this.settings = getUserPreference()
|
this.settings = getUserPreference()
|
||||||
this.delay = this.settings.default_delay || 4
|
this.delay = this.settings.default_delay || 4
|
||||||
|
this.delim = this.settings.csv_delim || ","
|
||||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||||
if (this.settings.shopping_auto_sync) {
|
if (this.settings.shopping_auto_sync) {
|
||||||
window.addEventListener("online", this.updateOnlineStatus)
|
window.addEventListener("online", this.updateOnlineStatus)
|
||||||
|
33
vue/src/components/Buttons/DownloadCSV.vue
Normal file
33
vue/src/components/Buttons/DownloadCSV.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
|
||||||
|
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "DownloadCSV",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
items: { type: Array },
|
||||||
|
name: { type: String },
|
||||||
|
icon: { type: String },
|
||||||
|
label: { type: String },
|
||||||
|
button: { type: Boolean, default: false },
|
||||||
|
delim: { type: String, default: "," },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
downloadFile() {
|
||||||
|
let csvContent = "data:text/csv;charset=utf-8,"
|
||||||
|
csvContent += [Object.keys(this.items[0]).join(this.delim), ...this.items.map((x) => Object.values(x).join(this.delim))].join("\n").replace(/(^\[)|(\]$)/gm, "")
|
||||||
|
|
||||||
|
const data = encodeURI(csvContent)
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.setAttribute("href", data)
|
||||||
|
link.setAttribute("download", "export.csv")
|
||||||
|
link.click()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -265,11 +265,9 @@
|
|||||||
"CategoryInstruction": "Drag categories to change the order categories appear in shopping list.",
|
"CategoryInstruction": "Drag categories to change the order categories appear in shopping list.",
|
||||||
"shopping_recent_days_desc": "Days of recent shopping list entries to display.",
|
"shopping_recent_days_desc": "Days of recent shopping list entries to display.",
|
||||||
"shopping_recent_days": "Recent Days",
|
"shopping_recent_days": "Recent Days",
|
||||||
"Coming_Soon": "Coming-Soon",
|
|
||||||
"Auto_Planner": "Auto-Planner",
|
|
||||||
"New_Cookbook": "New cookbook",
|
|
||||||
"Hide_Keyword": "Hide keywords",
|
|
||||||
"Clear": "Clear",
|
|
||||||
"create_shopping_new": "NEW: Add to Shopping List",
|
"create_shopping_new": "NEW: Add to Shopping List",
|
||||||
"download_pdf": "Download PDF"
|
"download_pdf": "Download PDF",
|
||||||
|
"download_csv": "Download CSV",
|
||||||
|
"csv_delim_help": "Delimiter to use for CSV exports.",
|
||||||
|
"csv_delim_label": "CSV Delimiter"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user