diff --git a/cookbook/forms.py b/cookbook/forms.py index 2cea2d07..7179c2c4 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -36,7 +36,10 @@ class UserPreferenceForm(forms.ModelForm): prefix = 'preference' 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) self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all() @@ -483,7 +486,7 @@ class ShoppingPreferenceForm(forms.ModelForm): fields = ( '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 = { @@ -498,6 +501,7 @@ class ShoppingPreferenceForm(forms.ModelForm): 'default_delay': _('Default number of hours to delay a shopping list entry.'), 'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'), 'shopping_recent_days': _('Days of recent shopping list entries to display.'), + 'csv_delim': _('Delimiter to use for CSV exports.'), } labels = { 'shopping_share': _('Share Shopping List'), @@ -507,7 +511,8 @@ class ShoppingPreferenceForm(forms.ModelForm): 'mealplan_autoinclude_related': _('Include Related'), 'default_delay': _('Default Delay Hours'), 'filter_to_supermarket': _('Filter to Supermarket'), - 'shopping_recent_days': _('Recent Days') + 'shopping_recent_days': _('Recent Days'), + 'csv_delim': _('CSV Delimiter'), } widgets = { diff --git a/cookbook/migrations/0162_userpreference_csv_delim.py b/cookbook/migrations/0162_userpreference_csv_delim.py new file mode 100644 index 00000000..e296ba95 --- /dev/null +++ b/cookbook/migrations/0162_userpreference_csv_delim.py @@ -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), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 57b5d878..77affc5d 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -333,6 +333,7 @@ class UserPreference(models.Model, PermissionModelMixin): filter_to_supermarket = models.BooleanField(default=False) default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4) shopping_recent_days = models.PositiveIntegerField(default=7) + csv_delim = models.CharField(max_length=2, default=",") created_at = models.DateTimeField(auto_now_add=True) space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index e5a4b9cc..3401d217 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -155,7 +155,7 @@ class FoodInheritFieldSerializer(UniqueFieldsMixin): class UserPreferenceSerializer(serializers.ModelSerializer): # food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True) 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): return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data @@ -165,14 +165,19 @@ class UserPreferenceSerializer(serializers.ModelSerializer): raise NotFound() 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: model = UserPreference fields = ( 'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share', 'ingredient_decimals', '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): diff --git a/vue/src/apps/ShoppingListView/ShoppingListView.vue b/vue/src/apps/ShoppingListView/ShoppingListView.vue index a265dcd5..f71063e8 100644 --- a/vue/src/apps/ShoppingListView/ShoppingListView.vue +++ b/vue/src/apps/ShoppingListView/ShoppingListView.vue @@ -10,10 +10,11 @@ @@ -346,6 +347,19 @@ +
+
{{ $t("csv_delim_label") }}
+
+ +
+
+
+
+ + {{ $t("csv_delim_help") }} + +
+
@@ -451,6 +465,7 @@ import ContextMenu from "@/components/ContextMenu/ContextMenu" import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem" import ShoppingLineItem from "@/components/ShoppingLineItem" import DownloadPDF from "@/components/Buttons/DownloadPDF" +import DownloadCSV from "@/components/Buttons/DownloadCSV" import GenericMultiselect from "@/components/GenericMultiselect" import GenericPill from "@/components/GenericPill" import LookupInput from "@/components/Modals/LookupInput" @@ -465,7 +480,7 @@ Vue.use(BootstrapVue) export default { name: "ShoppingListView", mixins: [ApiMixin], - components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput, DownloadPDF }, + components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput, DownloadPDF, DownloadCSV }, data() { return { @@ -488,6 +503,7 @@ export default { mealplan_autoexclude_onhand: true, filter_to_supermarket: false, shopping_recent_days: 7, + csv_delim: ",", }, new_supermarket: { entrymode: false, value: undefined, editmode: undefined }, new_category: { entrymode: false, value: undefined }, @@ -570,6 +586,11 @@ export default { }) return groups }, + csvData() { + return this.items.map((x) => { + return { amount: x.amount, unit: x.unit?.name ?? "", food: x.food?.name ?? "" } + }) + }, defaultDelay() { return Number(getUserPreference("default_delay")) || 2 }, @@ -595,7 +616,6 @@ export default { supermarketCategory() { return this.new_supermarket.editmode ? this.new_supermarket.value.category_to_supermarket : this.shopping_categories }, - notSupermarketCategory() { let supercats = this.new_supermarket.value.category_to_supermarket .map((x) => x.category) @@ -645,6 +665,7 @@ export default { this.settings = getUserPreference() this.delay = this.settings.default_delay || 4 + this.delim = this.settings.csv_delim || "," this.supermarket_categories_only = this.settings.filter_to_supermarket if (this.settings.shopping_auto_sync) { window.addEventListener("online", this.updateOnlineStatus) diff --git a/vue/src/components/Buttons/DownloadCSV.vue b/vue/src/components/Buttons/DownloadCSV.vue new file mode 100644 index 00000000..d85947ba --- /dev/null +++ b/vue/src/components/Buttons/DownloadCSV.vue @@ -0,0 +1,33 @@ + + + diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 80bba4da..7b909ca1 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -265,11 +265,9 @@ "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": "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", - "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" }