Merge branch 'feature/export-progress' into develop
This commit is contained in:
commit
a0892470e1
@ -45,7 +45,8 @@ SHOPPING_MIN_AUTOSYNC_INTERVAL=5
|
||||
# Default for user setting sticky navbar
|
||||
# STICKY_NAV_PREF_DEFAULT=1
|
||||
|
||||
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
|
||||
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
|
||||
# Be sure to not have a trailing slash: e.g. '/recipes' instead of '/recipes/'
|
||||
# SCRIPT_NAME=/recipes
|
||||
|
||||
# If staticfiles are stored at a different location uncomment and change accordingly, MUST END IN /
|
||||
@ -150,5 +151,6 @@ REVERSE_PROXY_AUTH=0
|
||||
# Disabled by default, uncomment to enable
|
||||
# ENABLE_PDF_EXPORT=1
|
||||
|
||||
# Duration to keep the cached export file
|
||||
EXPORT_FILE_CACHE_DURATION=600
|
||||
# Duration to keep the cached export file (in seconds)
|
||||
EXPORT_FILE_CACHE_DURATION=300
|
||||
|
||||
|
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,15 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Version
|
||||
Please provide your current version (can be found on the system page since v0.8.4)
|
||||
Version:
|
||||
|
||||
### Bug description
|
||||
A clear and concise description of what the bug is.
|
81
.github/ISSUE_TEMPLATE/bug_report.md.bak
vendored
Normal file
81
.github/ISSUE_TEMPLATE/bug_report.md.bak
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Version
|
||||
<!-- Please provide your current version (can be found on the system page since v0.8.4). -->
|
||||
**Tandoor-Version:**
|
||||
|
||||
## Setup configuration
|
||||
<!--Please tick all boxes which apply to your configuration. Feel free to provide additional information below.
|
||||
To tick boxes here, simply put an X inside the brackets below -->
|
||||
|
||||
### Setup
|
||||
- [ ] Docker / Docker-Compose
|
||||
- [ ] Unraid
|
||||
- [ ] Synology
|
||||
- [ ] Kubernetes
|
||||
- [ ] Manual setup
|
||||
- [ ] Others (please state below)
|
||||
|
||||
### Reverse Proxy
|
||||
- [ ] No reverse proxy
|
||||
- [ ] jwilder's nginx proxy
|
||||
- [ ] Nginx proxy manager (NPM)
|
||||
- [ ] SWAG
|
||||
- [ ] Caddy
|
||||
- [ ] Traefik
|
||||
- [ ] Others (please state below)
|
||||
|
||||
<!-- Please provide additional information if possible -->
|
||||
**Additional information:**
|
||||
|
||||
## Bug description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
|
||||
## Logs
|
||||
<!-- *(Remove this section entirely if no logs are available or necessary for your issue)*
|
||||
To get the most information about your issue, set DEBUG=1 (e.g. in your `.env` file if using docker-compose) and try to reproduce the issue afterwards.
|
||||
|
||||
Please put your logs into the expandable section below and use code quotation for all logs! Usage: Put three backticks in front and after the log, like this:
|
||||
` ``` <Many lines of log messages ``` `
|
||||
|
||||
Feel free to remove parts if you don't fill them out.
|
||||
-->
|
||||
|
||||
<details>
|
||||
<summary>Web-Container-Logs</summary>
|
||||
|
||||
<!-- *Put your logs inside here (leave the code quotations in takt):* -->
|
||||
|
||||
```
|
||||
Replace me with logs
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DB-Container-Logs</summary>
|
||||
|
||||
<!-- *Put your logs inside here (leave the code quotations in takt):* -->
|
||||
|
||||
```
|
||||
Replace me with logs
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Nginx-Container-Logs <!-- if you use one --></summary>
|
||||
|
||||
<!-- *Put your logs inside here (leave the code quotations in takt):* -->
|
||||
|
||||
```
|
||||
Replace me with logs
|
||||
```
|
||||
</details>
|
64
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: Bug Report
|
||||
description: "Create a report to help us improve"
|
||||
#title: ""
|
||||
#labels: ["Bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Tandoor Version
|
||||
description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: setup
|
||||
attributes:
|
||||
label: Setup
|
||||
description: "How is your Tandoor instance set up?"
|
||||
options:
|
||||
- Docker / Docker-Compose
|
||||
- Unraid
|
||||
- Synology
|
||||
- Kubernetes
|
||||
- Manual Setup
|
||||
- Others (please state below)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: reverse-proxy
|
||||
attributes:
|
||||
label: "Reverse Proxy"
|
||||
description: "What reverse proxy do you use with Tandoor?"
|
||||
options:
|
||||
- No reverse proxy
|
||||
- jwilder's nginx proxy
|
||||
- Nginx Proxy Manager (NPM)
|
||||
- SWAG
|
||||
- Caddy
|
||||
- Traefik
|
||||
- Apache2
|
||||
- Others (please state below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: other
|
||||
attributes:
|
||||
label: Other
|
||||
description: "In case you chose 'Others' above, please provide more info here."
|
||||
- type: textarea
|
||||
id: bug-descr
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: "Please accurately describe the bug you encountered."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs
|
||||
description: Please copy and paste any relevant logs. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: FAQs
|
||||
url: https://docs.tandoor.dev/faq/
|
||||
about: Please take a look at the FAQs before creating a bug ticket.
|
40
.github/ISSUE_TEMPLATE/doc_issue.yml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/doc_issue.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: Documentation Issue
|
||||
description: "Create a report to help us improve"
|
||||
#title: ""
|
||||
labels: ["documentation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this documentation issue report!
|
||||
- type: input
|
||||
id: docs-link
|
||||
attributes:
|
||||
label: Documentation link
|
||||
description: "Please provide a link to the corresponding documentation site on docs.tandoor.dev"
|
||||
- type: dropdown
|
||||
id: section
|
||||
attributes:
|
||||
label: Affected section
|
||||
description: "What part of the documentation is the issue about?"
|
||||
options:
|
||||
- Installation
|
||||
- Features
|
||||
- System
|
||||
- FAQ
|
||||
- Does not exist yet
|
||||
- Other (please state below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: other
|
||||
attributes:
|
||||
label: Other
|
||||
description: "In case you chose 'Other' above, please provide more info here."
|
||||
- type: textarea
|
||||
id: descr
|
||||
attributes:
|
||||
label: Issue description
|
||||
description: "Please accurately describe the documentation issue you are seeing."
|
||||
validations:
|
||||
required: true
|
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: Feature Request
|
||||
description: "Suggest an idea for this project"
|
||||
#title: ""
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: "Is your feature request related to a problem? Please describe."
|
||||
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when..."
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "Describe the solution you'd like"
|
||||
description: "A clear and concise description of what you want to happen."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: "Describe alternatives you've considered"
|
||||
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: "Additional context"
|
||||
description: "Add any other context or screenshots about the feature request here."
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: "Contribute"
|
||||
description: "Are you willing and able to help develop this feature?"
|
||||
options:
|
||||
- label: "Yes"
|
||||
- label: "Partly"
|
||||
- label: "No"
|
82
.github/ISSUE_TEMPLATE/help_request.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/help_request.yml
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
name: Help request
|
||||
description: "If there is anything wrong with your setup"
|
||||
#title: ""
|
||||
labels: ["setup issue"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this help request!
|
||||
- type: textarea
|
||||
id: issue
|
||||
attributes:
|
||||
label: Issue
|
||||
description: "Please describe your problem here."
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Tandoor Version
|
||||
description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS Version
|
||||
description: "E.g. Ubuntu 20.02"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: setup
|
||||
attributes:
|
||||
label: Setup
|
||||
description: "How is your Tandoor instance set up?"
|
||||
options:
|
||||
- Docker / Docker-Compose
|
||||
- Unraid
|
||||
- Synology
|
||||
- Kubernetes
|
||||
- Manual Setup
|
||||
- Others (please state below)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: reverse-proxy
|
||||
attributes:
|
||||
label: "Reverse Proxy"
|
||||
description: "What reverse proxy do you use with Tandoor?"
|
||||
options:
|
||||
- No reverse proxy
|
||||
- jwilder's nginx proxy
|
||||
- Nginx Proxy Manager (NPM)
|
||||
- SWAG
|
||||
- Caddy
|
||||
- Traefik
|
||||
- Others (please state below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: other
|
||||
attributes:
|
||||
label: Other
|
||||
description: "In case you chose 'Others' above or have more info, please provide additional details here."
|
||||
- type: textarea
|
||||
id: env
|
||||
attributes:
|
||||
label: Environment file
|
||||
description: "Please include your `.env` config file (**make sure to remove/replace all secrets**)"
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: docker-compose
|
||||
attributes:
|
||||
label: Docker-Compose file
|
||||
description: "When running with docker compose please provide your `docker-compose.yml`"
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs
|
||||
description: "If you feel like there is anything interesting please post the output of `docker-compose logs` at container startup and when the issue happens."
|
||||
render: shell
|
36
.github/ISSUE_TEMPLATE/website_import.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/website_import.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Website Import
|
||||
description: "Anything related to website imports"
|
||||
#title: ""
|
||||
#labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this website import form!
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Tandoor Version
|
||||
description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: Import URL
|
||||
description: "Exact URL you are trying to import from."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-descr
|
||||
attributes:
|
||||
label: "When did the issue happen?"
|
||||
description: "When pressing the search button with the url / when importing after the page has loaded / ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Response / message shown
|
||||
description: Please copy and paste any relevant logs or responses / messages which are shown in Tandoor. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Continous Integration
|
||||
name: Continuous Integration
|
||||
|
||||
on: [push]
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continuous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||
|
@ -7,4 +7,4 @@ Since this software is still considered beta/WIP support is always only given fo
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please open a normal public issue if you have any security related concerns. If you feel like the issue should not be discussed in
|
||||
public just open a generic issue and we will discuss further communitcation there (since GitHub does not allow everyone to create a security advisory :/).
|
||||
public just open a generic issue and we will discuss further communication there (since GitHub does not allow everyone to create a security advisory :/).
|
||||
|
@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField
|
||||
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
|
||||
TelegramBot, Unit, UserFile, UserPreference, ViewLog)
|
||||
TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation)
|
||||
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
@ -29,11 +29,52 @@ admin.site.register(User, CustomUserAdmin)
|
||||
admin.site.unregister(Group)
|
||||
|
||||
|
||||
@admin.action(description='Delete all data from a space')
|
||||
def delete_space_action(modeladmin, request, queryset):
|
||||
for space in queryset:
|
||||
CookLog.objects.filter(space=space).delete()
|
||||
ViewLog.objects.filter(space=space).delete()
|
||||
ImportLog.objects.filter(space=space).delete()
|
||||
BookmarkletImport.objects.filter(space=space).delete()
|
||||
|
||||
Comment.objects.filter(recipe__space=space).delete()
|
||||
Keyword.objects.filter(space=space).delete()
|
||||
Ingredient.objects.filter(space=space).delete()
|
||||
Food.objects.filter(space=space).delete()
|
||||
Unit.objects.filter(space=space).delete()
|
||||
Step.objects.filter(space=space).delete()
|
||||
NutritionInformation.objects.filter(space=space).delete()
|
||||
RecipeBookEntry.objects.filter(book__space=space).delete()
|
||||
RecipeBook.objects.filter(space=space).delete()
|
||||
MealType.objects.filter(space=space).delete()
|
||||
MealPlan.objects.filter(space=space).delete()
|
||||
ShareLink.objects.filter(space=space).delete()
|
||||
Recipe.objects.filter(space=space).delete()
|
||||
|
||||
RecipeImport.objects.filter(space=space).delete()
|
||||
SyncLog.objects.filter(sync__space=space).delete()
|
||||
Sync.objects.filter(space=space).delete()
|
||||
Storage.objects.filter(space=space).delete()
|
||||
|
||||
ShoppingListEntry.objects.filter(shoppinglist__space=space).delete()
|
||||
ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete()
|
||||
ShoppingList.objects.filter(space=space).delete()
|
||||
|
||||
SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete()
|
||||
SupermarketCategory.objects.filter(space=space).delete()
|
||||
Supermarket.objects.filter(space=space).delete()
|
||||
|
||||
InviteLink.objects.filter(space=space).delete()
|
||||
UserFile.objects.filter(space=space).delete()
|
||||
Automation.objects.filter(space=space).delete()
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
date_hierarchy = 'created_at'
|
||||
actions = [delete_space_action]
|
||||
|
||||
|
||||
admin.site.register(Space, SpaceAdmin)
|
||||
@ -128,7 +169,7 @@ def sort_tree(modeladmin, request, queryset):
|
||||
class KeywordAdmin(TreeAdmin):
|
||||
form = movenodeform_factory(Keyword)
|
||||
ordering = ('space', 'path',)
|
||||
search_fields = ('name', )
|
||||
search_fields = ('name',)
|
||||
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
|
||||
|
||||
|
||||
@ -136,8 +177,8 @@ admin.site.register(Keyword, KeywordAdmin)
|
||||
|
||||
|
||||
class StepAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'type', 'order')
|
||||
search_fields = ('name', 'type')
|
||||
list_display = ('name', 'order',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
admin.site.register(Step, StepAdmin)
|
||||
@ -171,13 +212,15 @@ class RecipeAdmin(admin.ModelAdmin):
|
||||
admin.site.register(Recipe, RecipeAdmin)
|
||||
|
||||
admin.site.register(Unit)
|
||||
|
||||
|
||||
# admin.site.register(FoodInheritField)
|
||||
|
||||
|
||||
class FoodAdmin(TreeAdmin):
|
||||
form = movenodeform_factory(Keyword)
|
||||
ordering = ('space', 'path',)
|
||||
search_fields = ('name', )
|
||||
search_fields = ('name',)
|
||||
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
|
||||
|
||||
|
||||
|
@ -358,8 +358,8 @@ class InviteLinkForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
space = self.cleaned_data['space']
|
||||
if space.max_users != 0 and (UserPreference.objects.filter(space=space).count() + InviteLink.objects.filter(
|
||||
space=space).filter(valid_until__gte=datetime.today()).count()) >= space.max_users:
|
||||
if space.max_users != 0 and (UserPreference.objects.filter(space=space).count() +
|
||||
InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=space).count()) >= space.max_users:
|
||||
raise ValidationError(_('Maximum number of users for this space reached.'))
|
||||
|
||||
def clean_email(self):
|
||||
@ -442,7 +442,7 @@ class SearchPreferenceForm(forms.ModelForm):
|
||||
|
||||
help_texts = {
|
||||
'search': _(
|
||||
'Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
|
||||
'Select type method of search. Click <a href="/docs/search/">here</a> for full description of choices.'),
|
||||
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
|
||||
'unaccent': _(
|
||||
'Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
|
||||
@ -461,7 +461,7 @@ class SearchPreferenceForm(forms.ModelForm):
|
||||
'lookup': _('Fuzzy Lookups'),
|
||||
'unaccent': _('Ignore Accent'),
|
||||
'icontains': _("Partial Match"),
|
||||
'istartswith': _("Starts Wtih"),
|
||||
'istartswith': _("Starts With"),
|
||||
'trigram': _("Fuzzy Search"),
|
||||
'fulltext': _("Full Text")
|
||||
}
|
||||
@ -535,10 +535,11 @@ class SpacePreferenceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Space
|
||||
|
||||
fields = ('food_inherit', 'reset_food_inherit',)
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count')
|
||||
|
||||
help_texts = {
|
||||
'food_inherit': _('Fields on food that should be inherited by default.'), }
|
||||
'food_inherit': _('Fields on food that should be inherited by default.'),
|
||||
'show_facet_count': _('Show recipe counts on search filters'), }
|
||||
|
||||
widgets = {
|
||||
'food_inherit': MultiSelectWidget
|
||||
|
@ -7,7 +7,7 @@ class Round(Func):
|
||||
|
||||
|
||||
def str2bool(v):
|
||||
if type(v) == bool:
|
||||
if type(v) == bool or v is None:
|
||||
return v
|
||||
else:
|
||||
return v.lower() in ("yes", "true", "1")
|
||||
|
@ -34,7 +34,7 @@ def has_group_permission(user, groups):
|
||||
"""
|
||||
Tests if a given user is member of a certain group (or any higher group)
|
||||
Superusers always bypass permission checks.
|
||||
Unauthenticated users cant be member of any group thus always return false.
|
||||
Unauthenticated users can't be member of any group thus always return false.
|
||||
:param user: django auth user object
|
||||
:param groups: list or tuple of groups the user should be checked for
|
||||
:return: True if user is in allowed groups, false otherwise
|
||||
@ -206,7 +206,7 @@ class CustomIsShared(permissions.BasePermission):
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# temporary hack to make old shopping list work with new shopping list
|
||||
if obj.__class__.__name__ == 'ShoppingList':
|
||||
if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
|
||||
return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
|
||||
return is_object_shared(request.user, obj)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -38,6 +38,7 @@ def shopping_helper(qs, request):
|
||||
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
|
||||
|
||||
|
||||
# TODO refactor as class
|
||||
def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
|
||||
"""
|
||||
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
|
||||
@ -78,7 +79,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
elif ingredients:
|
||||
ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
|
||||
else:
|
||||
ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
|
||||
ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
|
||||
|
||||
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
||||
ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
@ -100,9 +101,9 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
if ingredients.filter(food__recipe=x).exists():
|
||||
for ing in ingredients.filter(food__recipe=x):
|
||||
if exclude_onhand:
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
else:
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, space=space)
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
|
||||
for i in [x for x in x_ing]:
|
||||
ShoppingListEntry.objects.create(
|
||||
list_recipe=list_recipe,
|
||||
@ -125,7 +126,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
||||
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
||||
|
||||
# if servings have changed, update the ShoppingListRecipe and existing Entrys
|
||||
# if servings have changed, update the ShoppingListRecipe and existing Entries
|
||||
if servings <= 0:
|
||||
servings = 1
|
||||
|
||||
@ -137,7 +138,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
||||
sle.save()
|
||||
|
||||
# add any missing Entrys
|
||||
# add any missing Entries
|
||||
for i in [x for x in add_ingredients if x.food]:
|
||||
|
||||
ShoppingListEntry.objects.create(
|
||||
|
@ -2,7 +2,6 @@ import json
|
||||
from io import BytesIO, StringIO
|
||||
from re import match
|
||||
from zipfile import ZipFile
|
||||
from django.utils.text import get_valid_filename
|
||||
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
@ -60,13 +59,12 @@ class Default(Integration):
|
||||
|
||||
recipe_zip_obj.close()
|
||||
|
||||
export_zip_obj.writestr(get_valid_filename(r.name) + '.zip', recipe_zip_stream.getvalue())
|
||||
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
|
||||
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(r)
|
||||
el.save()
|
||||
|
||||
|
||||
export_zip_obj.close()
|
||||
|
||||
return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
|
@ -46,7 +46,7 @@ class Integration:
|
||||
try:
|
||||
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
|
||||
name = f'Import {int(last_kw.name.replace("Import ", "")) + 1}'
|
||||
except ObjectDoesNotExist:
|
||||
except (ObjectDoesNotExist, ValueError):
|
||||
name = 'Import 1'
|
||||
|
||||
parent, created = Keyword.objects.get_or_create(name='Import', space=request.space)
|
||||
@ -57,7 +57,7 @@ class Integration:
|
||||
icon=icon,
|
||||
space=request.space
|
||||
)
|
||||
except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
||||
except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
||||
self.keyword = parent.add_child(
|
||||
name=f'{name} {str(uuid.uuid4())[0:8]}',
|
||||
description=description,
|
||||
@ -98,6 +98,10 @@ class Integration:
|
||||
el.running = False
|
||||
el.save()
|
||||
|
||||
response = HttpResponse(export_file, content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
||||
return response
|
||||
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
"""
|
||||
@ -269,7 +273,6 @@ class Integration:
|
||||
"""
|
||||
raise NotImplementedError('Method not implemented in integration')
|
||||
|
||||
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
"""
|
||||
Takes a list of recipe object and converts it to a array containing each file.
|
||||
|
@ -27,10 +27,10 @@ class RecetteTek(Integration):
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
|
||||
# Create initial recipe with just a title and a decription
|
||||
# Create initial recipe with just a title and a description
|
||||
recipe = Recipe.objects.create(name=file['title'], created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
# set the description as an empty string for later use for the source URL, incase there is no description text.
|
||||
# set the description as an empty string for later use for the source URL, in case there is no description text.
|
||||
recipe.description = ''
|
||||
|
||||
try:
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||
self.stdout.write(self.style.WARNING(_('Only Postgress databases use full text search, no index to rebuild')))
|
||||
self.stdout.write(self.style.WARNING(_('Only Postgresql databases use full text search, no index to rebuild')))
|
||||
|
||||
try:
|
||||
language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
|
@ -140,5 +140,10 @@ class Migration(migrations.Migration):
|
||||
old_name='ignore_shopping',
|
||||
new_name='food_onhand',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='show_facet_count',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(copy_values_to_sle),
|
||||
]
|
||||
|
19
cookbook/migrations/0164_space_show_facet_count.py
Normal file
19
cookbook/migrations/0164_space_show_facet_count.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-17 22:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0163_auto_20220105_0758'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# migrations.AddField(
|
||||
# model_name='space',
|
||||
# name='show_facet_count',
|
||||
# field=models.BooleanField(default=False),
|
||||
# ),
|
||||
# removed due to quick fix in 0159 migration to maintain correct order
|
||||
]
|
17
cookbook/migrations/0165_remove_step_type.py
Normal file
17
cookbook/migrations/0165_remove_step_type.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-18 19:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0164_space_show_facet_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='step',
|
||||
name='type',
|
||||
),
|
||||
]
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-20 14:39
|
||||
|
||||
from django.db import migrations, models
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
def add_default_trigram(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
UserPreference = apps.get_model('cookbook', 'UserPreference')
|
||||
|
||||
UserPreference.objects.all().update(shopping_add_onhand=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0165_remove_step_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='shopping_add_onhand',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(add_default_trigram),
|
||||
]
|
18
cookbook/migrations/0167_userpreference_left_handed.py
Normal file
18
cookbook/migrations/0167_userpreference_left_handed.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-20 22:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0166_alter_userpreference_shopping_add_onhand'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='left_handed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
@ -77,7 +77,10 @@ class TreeManager(MP_NodeManager):
|
||||
for field in many_to_many:
|
||||
field_model = getattr(obj, field).model
|
||||
for related_obj in many_to_many[field]:
|
||||
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
|
||||
if isinstance(related_obj, User):
|
||||
getattr(obj, field).add(field_model.objects.get(id=related_obj.id))
|
||||
else:
|
||||
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
|
||||
return obj, True
|
||||
except IntegrityError as e:
|
||||
if 'Key (path)' in e.args[0]:
|
||||
@ -148,6 +151,7 @@ class TreeModel(MP_Node):
|
||||
return super().add_root(**kwargs)
|
||||
|
||||
# i'm 99% sure there is a more idiomatic way to do this subclassing MP_NodeQuerySet
|
||||
@staticmethod
|
||||
def include_descendants(queryset=None, filter=None):
|
||||
"""
|
||||
:param queryset: Model Queryset to add descendants
|
||||
@ -244,6 +248,7 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
allow_sharing = models.BooleanField(default=True)
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
show_facet_count = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -332,8 +337,9 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
mealplan_autoadd_shopping = models.BooleanField(default=False)
|
||||
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
||||
mealplan_autoinclude_related = models.BooleanField(default=True)
|
||||
shopping_add_onhand = models.BooleanField(default=True)
|
||||
shopping_add_onhand = models.BooleanField(default=False)
|
||||
filter_to_supermarket = models.BooleanField(default=False)
|
||||
left_handed = 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=",")
|
||||
@ -510,7 +516,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
|
||||
@staticmethod
|
||||
def reset_inheritance(space=None):
|
||||
# resets inheritted fields to the space defaults and updates all inheritted fields to root object values
|
||||
# resets inherited fields to the space defaults and updates all inherited fields to root object values
|
||||
inherit = space.food_inherit.all()
|
||||
|
||||
# remove all inherited fields from food
|
||||
@ -571,17 +577,7 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
||||
|
||||
|
||||
class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixin):
|
||||
TEXT = 'TEXT'
|
||||
TIME = 'TIME'
|
||||
FILE = 'FILE'
|
||||
RECIPE = 'RECIPE'
|
||||
|
||||
name = models.CharField(max_length=128, default='', blank=True)
|
||||
type = models.CharField(
|
||||
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
|
||||
default=TEXT,
|
||||
max_length=16
|
||||
)
|
||||
instruction = models.TextField(blank=True)
|
||||
ingredients = models.ManyToManyField(Ingredient, blank=True)
|
||||
time = models.IntegerField(default=0, blank=True)
|
||||
@ -613,9 +609,7 @@ class NutritionInformation(models.Model, PermissionModelMixin):
|
||||
)
|
||||
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
source = models.CharField(
|
||||
max_length=512, default="", null=True, blank=True
|
||||
)
|
||||
source = models.CharField( max_length=512, default="", null=True, blank=True)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
@ -624,6 +618,15 @@ class NutritionInformation(models.Model, PermissionModelMixin):
|
||||
return f'Nutrition {self.pk}'
|
||||
|
||||
|
||||
# class NutritionType(models.Model, PermissionModelMixin):
|
||||
# name = models.CharField(max_length=128)
|
||||
# icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
# description = models.CharField(max_length=512, blank=True, null=True)
|
||||
#
|
||||
# space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
# objects = ScopedManager(space='space')
|
||||
|
||||
|
||||
class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=512, blank=True, null=True)
|
||||
|
@ -29,7 +29,11 @@ class Nextcloud(Provider):
|
||||
client = Nextcloud.get_client(monitor.storage)
|
||||
|
||||
files = client.list(monitor.path)
|
||||
files.pop(0) # remove first element because its the folder itself
|
||||
|
||||
try:
|
||||
files.pop(0) # remove first element because its the folder itself
|
||||
except IndexError:
|
||||
pass # folder is empty, no recipes will be imported
|
||||
|
||||
import_count = 0
|
||||
for file in files:
|
||||
|
@ -12,6 +12,7 @@ from rest_framework import serializers
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.shopping_helper import list_from_recipe
|
||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
|
||||
FoodInheritField, ImportLog, ExportLog, Ingredient, Keyword, MealPlan, MealType,
|
||||
@ -21,13 +22,18 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Fo
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile,
|
||||
UserPreference, ViewLog)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
from recipes.settings import MEDIA_URL
|
||||
|
||||
|
||||
class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
# adds image and recipe count to serializer when query param extended=1
|
||||
image = serializers.SerializerMethodField('get_image')
|
||||
numrecipe = serializers.SerializerMethodField('count_recipes')
|
||||
# ORM path to this object from Recipe
|
||||
recipe_filter = None
|
||||
# list of ORM paths to any image
|
||||
images = None
|
||||
|
||||
image = serializers.SerializerMethodField('get_image')
|
||||
numrecipe = serializers.ReadOnlyField(source='recipe_count')
|
||||
|
||||
def get_fields(self, *args, **kwargs):
|
||||
fields = super().get_fields(*args, **kwargs)
|
||||
@ -37,8 +43,7 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
try:
|
||||
if bool(int(
|
||||
self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
|
||||
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError) as e:
|
||||
pass
|
||||
@ -50,24 +55,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
return fields
|
||||
|
||||
def get_image(self, obj):
|
||||
# TODO add caching
|
||||
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(
|
||||
image__isnull=True).exclude(image__exact='')
|
||||
try:
|
||||
if recipes.count() == 0 and obj.has_children():
|
||||
obj__in = self.recipe_filter + '__in'
|
||||
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(
|
||||
image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
|
||||
except AttributeError:
|
||||
# probably not a tree
|
||||
pass
|
||||
if recipes.count() != 0:
|
||||
return random.choice(recipes).image.url
|
||||
else:
|
||||
return None
|
||||
|
||||
def count_recipes(self, obj):
|
||||
return Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).count()
|
||||
if obj.recipe_image:
|
||||
return MEDIA_URL + obj.recipe_image
|
||||
|
||||
|
||||
class CustomDecimalField(serializers.Field):
|
||||
@ -98,7 +87,11 @@ class CustomOnHandField(serializers.Field):
|
||||
return instance
|
||||
|
||||
def to_representation(self, obj):
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
shared_users = None
|
||||
if request := self.context.get('request', None):
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
@ -165,13 +158,19 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FoodInheritField
|
||||
fields = ('id', 'name', 'field', )
|
||||
fields = ('id', 'name', 'field',)
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||
class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
|
||||
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
|
||||
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
||||
|
||||
def get_food_children_exist(self, obj):
|
||||
space = getattr(self.context.get('request', None), 'space', None)
|
||||
return Food.objects.filter(depth__gt=0, space=space).exists()
|
||||
|
||||
def create(self, validated_data):
|
||||
if not validated_data.get('user', None):
|
||||
@ -183,10 +182,10 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = (
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
|
||||
'filter_to_supermarket', 'shopping_add_onhand'
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
|
||||
)
|
||||
|
||||
|
||||
@ -379,14 +378,16 @@ class RecipeSimpleSerializer(serializers.ModelSerializer):
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
|
||||
shopping = serializers.SerializerMethodField('get_shopping_status')
|
||||
# shopping = serializers.SerializerMethodField('get_shopping_status')
|
||||
shopping = serializers.ReadOnlyField(source='shopping_status')
|
||||
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||
food_onhand = CustomOnHandField(required=False, allow_null=True)
|
||||
|
||||
recipe_filter = 'steps__ingredients__food'
|
||||
images = ['recipe__image']
|
||||
|
||||
def get_shopping_status(self, obj):
|
||||
return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
|
||||
# def get_shopping_status(self, obj):
|
||||
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
@ -396,15 +397,20 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
|
||||
name=validated_data.pop('supermarket_category')['name'],
|
||||
space=self.context['request'].space)
|
||||
onhand = validated_data.get('food_onhand', None)
|
||||
onhand = validated_data.pop('food_onhand', None)
|
||||
|
||||
# assuming if on hand for user also onhand for shopping_share users
|
||||
if not onhand is None:
|
||||
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||
if onhand:
|
||||
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
|
||||
if self.instance:
|
||||
onhand_users = self.instance.onhand_users.all()
|
||||
else:
|
||||
validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users))
|
||||
onhand_users = []
|
||||
if onhand:
|
||||
validated_data['onhand_users'] = list(onhand_users) + shared_users
|
||||
else:
|
||||
validated_data['onhand_users'] = list(set(onhand_users) - set(shared_users))
|
||||
|
||||
obj, created = Food.objects.get_or_create(**validated_data)
|
||||
return obj
|
||||
|
||||
@ -425,7 +431,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
model = Food
|
||||
fields = (
|
||||
'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name'
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping'
|
||||
)
|
||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||
|
||||
@ -472,12 +478,12 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
# check if root type is recipe to prevent infinite recursion
|
||||
# can be improved later to allow multi level embedding
|
||||
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
||||
return StepRecipeSerializer(obj.step_recipe).data
|
||||
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = (
|
||||
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
|
||||
'id', 'name', 'instruction', 'ingredients', 'ingredients_markdown',
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data', 'numrecipe'
|
||||
)
|
||||
|
||||
@ -493,6 +499,10 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||
carbohydrates = CustomDecimalField()
|
||||
fats = CustomDecimalField()
|
||||
proteins = CustomDecimalField()
|
||||
calories = CustomDecimalField()
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
@ -516,7 +526,7 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
|
||||
|
||||
def get_recipe_last_cooked(self, obj):
|
||||
try:
|
||||
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
|
||||
last = obj.cooklog_set.filter(created_by=self.context['request'].user).order_by('created_at').last()
|
||||
if last:
|
||||
return last.created_at
|
||||
except TypeError:
|
||||
@ -525,7 +535,7 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
|
||||
|
||||
# TODO make days of new recipe a setting
|
||||
def is_recipe_new(self, obj):
|
||||
if obj.created_at > (timezone.now() - timedelta(days=7)):
|
||||
if getattr(obj, 'new_recipe', None) or obj.created_at > (timezone.now() - timedelta(days=7)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@ -536,6 +546,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||
rating = serializers.SerializerMethodField('get_recipe_rating')
|
||||
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
||||
new = serializers.SerializerMethodField('is_recipe_new')
|
||||
recent = serializers.ReadOnlyField()
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
@ -548,7 +559,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new'
|
||||
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
|
||||
)
|
||||
read_only_fields = ['image', 'created_by', 'created_at']
|
||||
|
||||
@ -681,7 +692,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if 'servings' in validated_data:
|
||||
# TODO remove once old shopping list
|
||||
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
|
||||
list_from_recipe(
|
||||
list_recipe=instance,
|
||||
servings=validated_data['servings'],
|
||||
@ -717,9 +729,9 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
def run_validation(self, data):
|
||||
if self.root.instance.__class__.__name__ == 'ShoppingListEntry':
|
||||
if (
|
||||
data.get('checked', False)
|
||||
and self.root.instance
|
||||
and not self.root.instance.checked
|
||||
data.get('checked', False)
|
||||
and self.root.instance
|
||||
and not self.root.instance.checked
|
||||
):
|
||||
# if checked flips from false to true set completed datetime
|
||||
data['completed_at'] = timezone.now()
|
||||
@ -755,7 +767,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
|
||||
'created_by', 'created_at', 'completed_at', 'delay_until'
|
||||
)
|
||||
read_only_fields = ('id', 'created_by', 'created_at',)
|
||||
read_only_fields = ('id', 'created_by', 'created_at',)
|
||||
|
||||
|
||||
# TODO deprecate
|
||||
@ -870,7 +882,7 @@ class AutomationSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# CORS, REST and Scopes aren't currently working
|
||||
# Scopes are evaluating before REST has authenticated the user assiging a None space
|
||||
# Scopes are evaluating before REST has authenticated the user assigning a None space
|
||||
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
|
||||
class BookmarkletImportSerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
@ -941,7 +953,7 @@ class StepExportSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
|
||||
fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
|
||||
|
||||
|
||||
class RecipeExportSerializer(WritableNestedModelSerializer):
|
||||
|
@ -72,7 +72,7 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
|
||||
return
|
||||
|
||||
inherit = inherit.values_list('field', flat=True)
|
||||
# apply changes from parent to instance for each inheritted field
|
||||
# apply changes from parent to instance for each inherited field
|
||||
if instance.parent and inherit.count() > 0:
|
||||
parent = instance.get_parent()
|
||||
if 'ignore_shopping' in inherit:
|
||||
@ -121,11 +121,3 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
|
||||
'servings': instance.servings
|
||||
}
|
||||
list_recipe = list_from_recipe(**kwargs)
|
||||
|
||||
|
||||
# user = self.context['request'].user
|
||||
# if user.userpreference.shopping_add_onhand:
|
||||
# if checked := validated_data.get('checked', None):
|
||||
# instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
|
||||
# elif checked == False:
|
||||
# instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)
|
||||
|
5
cookbook/static/themes/tandoor.min.css
vendored
5
cookbook/static/themes/tandoor.min.css
vendored
@ -10461,4 +10461,9 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([clas
|
||||
|
||||
.form-control-search {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5 !important;
|
||||
background: #b98766 !important;
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<hr>
|
||||
<hr>
|
||||
<form class="login" method="POST" action="{% url 'account_login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
@ -29,12 +29,16 @@
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
|
||||
{% if SIGNUP_ENABLED %}
|
||||
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if EMAIL_ENABLED %}
|
||||
<a class="btn btn-warning float-right d-none d-xl-block d-lg-block"
|
||||
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
|
||||
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
|
||||
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a
|
||||
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
@ -44,7 +48,7 @@
|
||||
|
||||
{% if socialaccount_providers %}
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<h5>{% trans "Social Login" %}</h5>
|
||||
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
|
||||
|
||||
@ -62,5 +66,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
$('#id_login').focus()
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -71,4 +71,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
$('#id_username').focus()
|
||||
</script>
|
||||
{% endblock %}
|
@ -54,7 +54,7 @@
|
||||
<h2>{% trans 'Formatting' %}</h2>
|
||||
<pre class="intro-code code-block"><code>
|
||||
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}
|
||||
{% trans 'or by leaving a blank line inbetween.' %}
|
||||
{% trans 'or by leaving a blank line in between.' %}
|
||||
|
||||
**{% trans 'This text is bold' %}**
|
||||
*{% trans 'This text is italic' %}*
|
||||
@ -70,7 +70,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}<br/>
|
||||
{% trans 'or by leaving a blank line inbetween.' %}<br/><br/>
|
||||
{% trans 'or by leaving a blank line in between.' %}<br/><br/>
|
||||
<b>{% trans 'This text is bold' %}</b><br/>
|
||||
<i>{% trans 'This text is italic' %}</i>
|
||||
<blockquote>
|
||||
@ -82,7 +82,7 @@
|
||||
|
||||
<br/>
|
||||
<h2>{% trans 'Lists' %}</h2>
|
||||
{% trans 'Lists can ordered or unorderd. It is <b>important to leave a blank line before the list!</b>' %}
|
||||
{% trans 'Lists can ordered or unordered. It is <b>important to leave a blank line before the list!</b>' %}
|
||||
<pre class="intro-code code-block"><code>
|
||||
{% trans 'Ordered List' %}
|
||||
|
||||
|
@ -27,7 +27,7 @@
|
||||
{% endblocktrans %}</p>
|
||||
<h4>{% trans 'Simple' %}</h4>
|
||||
<p> {% blocktrans %}
|
||||
Simple searches ignore punctuation and common words such as 'the', 'a', 'and'. And will treat seperate words as required.
|
||||
Simple searches ignore punctuation and common words such as 'the', 'a', 'and'. And will treat separate words as required.
|
||||
Searching for 'apple or flour' will return any recipe that includes both 'apple' and 'flour' anywhere in the fields that have been selected for a full text search.
|
||||
{% endblocktrans %}</p>
|
||||
<h4>{% trans 'Phrase' %}</h4>
|
||||
@ -39,7 +39,7 @@
|
||||
<p> {% blocktrans %}
|
||||
Web searches simulate functionality found on many web search sites supporting special syntax.
|
||||
Placing quotes around several words will convert those words into a phrase.
|
||||
'or' is recongized as searching for the word (or phrase) immediately before 'or' OR the word (or phrase) directly after.
|
||||
'or' is recognized as searching for the word (or phrase) immediately before 'or' OR the word (or phrase) directly after.
|
||||
'-' is recognized as searching for recipes that do not include the word (or phrase) that comes immediately after.
|
||||
For example searching for 'apple pie' or cherry -butter will return any recipe that includes the phrase 'apple pie' or the word 'cherry'
|
||||
in any field included in the full text search but exclude any recipe that has the word 'butter' in any field included.
|
||||
@ -59,7 +59,7 @@
|
||||
{% blocktrans %}
|
||||
Another approach to searching that also requires Postgresql is fuzzy search or trigram similarity. A trigram is a group of three consecutive characters.
|
||||
For example searching for 'apple' will create x trigrams 'app', 'ppl', 'ple' and will create a score of how closely words match the generated trigrams.
|
||||
One benefit of searching trigams is that a search for 'sandwich' will find mispelled words such as 'sandwhich' that would be missed by other methods.
|
||||
One benefit of searching trigams is that a search for 'sandwich' will find misspelled words such as 'sandwhich' that would be missed by other methods.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
|
@ -263,7 +263,7 @@
|
||||
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
||||
<div class="row" v-if="!onLine">
|
||||
<div class="col col-md-12">
|
||||
<div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not syncronize.' %}</div>
|
||||
<div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not synchronize.' %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -634,7 +634,7 @@
|
||||
console.log('updating recipe', this.shopping_list.recipes[i])
|
||||
recipe_promises.push(this.$http.post("{% url 'api:shoppinglistrecipe-list' %}", this.shopping_list.recipes[i], {}).then((response) => {
|
||||
let old_id = this.shopping_list.recipes[i].id
|
||||
console.log("list recipe create respose ", response.body)
|
||||
console.log("list recipe create response ", response.body)
|
||||
this.$set(this.shopping_list.recipes, i, response.body)
|
||||
for (let e of this.shopping_list.entries.filter(item => item.list_recipe === old_id)) {
|
||||
console.log("found recipe updating ID")
|
||||
@ -834,7 +834,7 @@
|
||||
this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => {
|
||||
for (let s of response.data.steps) {
|
||||
for (let i of s.ingredients) {
|
||||
if (!i.is_header && i.food !== null && i.food.food_onhand === false) {
|
||||
if (!i.is_header && i.food !== null && !i.food.ignore_food) {
|
||||
this.shopping_list.entries.push({
|
||||
'list_recipe': slr.id,
|
||||
'food': i.food,
|
||||
|
@ -498,6 +498,8 @@
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
placeholder="{% trans 'Select one' %}"
|
||||
tag-placeholder="{% trans 'Select' %}"
|
||||
label="text"
|
||||
@ -536,6 +538,8 @@
|
||||
:clear-on-select="true"
|
||||
:allow-empty="false"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
label="text"
|
||||
track-by="id"
|
||||
:multiple="false"
|
||||
@ -586,6 +590,8 @@
|
||||
:clear-on-select="true"
|
||||
:hide-selected="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
placeholder="{% trans 'Select one' %}"
|
||||
tag-placeholder="{% trans 'Add Keyword' %}"
|
||||
:taggable="true"
|
||||
@ -654,7 +660,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% url 'javascript-catalog' %}">
|
||||
</script>
|
||||
<script type="application/javascript">
|
||||
let csrftoken = Cookies.get('csrftoken');
|
||||
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
||||
@ -693,7 +700,8 @@
|
||||
import_duplicates: false,
|
||||
recipe_files: [],
|
||||
images: [],
|
||||
mode: 'url'
|
||||
mode: 'url',
|
||||
options_limit:25
|
||||
},
|
||||
directives: {
|
||||
tabindex: {
|
||||
@ -703,9 +711,9 @@
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.searchKeywords('')
|
||||
this.searchUnits('')
|
||||
this.searchIngredients('')
|
||||
// this.searchKeywords('')
|
||||
// this.searchUnits('')
|
||||
// this.searchIngredients('')
|
||||
let uri = window.location.search.substring(1);
|
||||
let params = new URLSearchParams(uri);
|
||||
q = params.get("id")
|
||||
@ -884,7 +892,20 @@
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
})
|
||||
// let apiFactory = new ApiApiFactory()
|
||||
|
||||
// this.keywords_loading = true
|
||||
// apiFactory
|
||||
// .listKeywords(query, undefined, undefined, 1, this.options_limit)
|
||||
// .then((response) => {
|
||||
// this.keywords = response.data.results
|
||||
// this.keywords_loading = false
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.log(err)
|
||||
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
// })
|
||||
},
|
||||
searchUnits: function (query) {
|
||||
this.units_loading = true
|
||||
@ -922,6 +943,29 @@
|
||||
console.log(err)
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
// let apiFactory = new ApiApiFactory()
|
||||
|
||||
// this.foods_loading = true
|
||||
// apiFactory
|
||||
// .listFoods(query, undefined, undefined, 1, this.options_limit)
|
||||
// .then((response) => {
|
||||
// this.foods = response.data.results
|
||||
|
||||
// if (this.recipe !== undefined) {
|
||||
// for (let s of this.recipe.steps) {
|
||||
// for (let i of s.ingredients) {
|
||||
// if (i.food !== null && i.food.id === undefined) {
|
||||
// this.foods.push(i.food)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.foods_loading = false
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
// })
|
||||
},
|
||||
deleteNode: function (node, item, e) {
|
||||
e.stopPropagation()
|
||||
|
@ -200,7 +200,7 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
|
||||
food = Food.objects.get(id=ingredients[2].food.id)
|
||||
food.onhand_users.add(user)
|
||||
food.save()
|
||||
food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food
|
||||
food = recipe.steps.exclude(step_recipe=None).first().step_recipe.steps.first().ingredients.first().food
|
||||
food = Food.objects.get(id=food.id)
|
||||
food.onhand_users.add(user)
|
||||
food.save()
|
||||
|
@ -269,10 +269,8 @@ class StepFactory(factory.django.DjangoModelFactory):
|
||||
return
|
||||
if kwargs.get('has_recipe', False):
|
||||
self.step_recipe = RecipeFactory(space=self.space)
|
||||
self.type = Step.RECIPE
|
||||
elif extracted:
|
||||
self.step_recipe = extracted
|
||||
self.type = Step.RECIPE
|
||||
|
||||
@factory.post_generation
|
||||
def ingredients(self, create, extracted, **kwargs):
|
||||
|
@ -12,8 +12,10 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db.models import Case, ProtectedError, Q, Value, When
|
||||
from django.db.models import (Case, Count, Exists, F, IntegerField, OuterRef, ProtectedError, Q,
|
||||
Subquery, Value, When)
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
@ -30,13 +32,14 @@ from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSetMixin
|
||||
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
|
||||
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
|
||||
CustomIsShare, CustomIsShared, CustomIsUser,
|
||||
group_required)
|
||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes
|
||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
|
||||
from cookbook.helper.recipe_url_import import get_from_scraper
|
||||
from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
|
||||
@ -100,7 +103,38 @@ class DefaultPagination(PageNumberPagination):
|
||||
max_page_size = 200
|
||||
|
||||
|
||||
class FuzzyFilterMixin(ViewSetMixin):
|
||||
class ExtendedRecipeMixin():
|
||||
'''
|
||||
ExtendedRecipe annotates a queryset with recipe_image and recipe_count values
|
||||
'''
|
||||
@classmethod
|
||||
def annotate_recipe(self, queryset=None, request=None, serializer=None, tree=False):
|
||||
extended = str2bool(request.query_params.get('extended', None))
|
||||
if extended:
|
||||
recipe_filter = serializer.recipe_filter
|
||||
images = serializer.images
|
||||
space = request.space
|
||||
|
||||
# add a recipe count annotation to the query
|
||||
# explanation on construction https://stackoverflow.com/a/43771738/15762829
|
||||
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(recipe_filter).annotate(count=Count('pk')).values('count')
|
||||
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
|
||||
|
||||
# add a recipe image annotation to the query
|
||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
if tree:
|
||||
image_children_subquery = Recipe.objects.filter(**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
else:
|
||||
image_children_subquery = None
|
||||
if images:
|
||||
queryset = queryset.annotate(recipe_image=Coalesce(*images, image_subquery, image_children_subquery))
|
||||
else:
|
||||
queryset = queryset.annotate(recipe_image=Coalesce(image_subquery, image_children_subquery))
|
||||
return queryset
|
||||
|
||||
|
||||
class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
schema = FilterSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
@ -112,18 +146,18 @@ class FuzzyFilterMixin(ViewSetMixin):
|
||||
if fuzzy:
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2)
|
||||
.order_by('-exact', '-trigram')
|
||||
.annotate(starts=Case(When(name__istartswith=query, then=(Value(.3, output_field=IntegerField()))), default=Value(0)))
|
||||
.annotate(trigram=TrigramSimilarity('name', query))
|
||||
.annotate(sort=F('starts')+F('trigram'))
|
||||
.order_by('-sort')
|
||||
)
|
||||
else:
|
||||
# TODO have this check unaccent search settings or other search preferences?
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(name__icontains=query).order_by('-exact', 'name')
|
||||
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(name__icontains=query).order_by('-starts', 'name')
|
||||
)
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
@ -141,12 +175,12 @@ class FuzzyFilterMixin(ViewSetMixin):
|
||||
if random:
|
||||
self.queryset = self.queryset.order_by("?")
|
||||
self.queryset = self.queryset[:int(limit)]
|
||||
return self.queryset
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class)
|
||||
|
||||
|
||||
class MergeMixin(ViewSetMixin):
|
||||
@decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
|
||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
@ decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
|
||||
@ decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
def merge(self, request, pk, target):
|
||||
self.description = f"Merge {self.basename} onto target {self.basename} with ID of [int]."
|
||||
|
||||
@ -211,7 +245,7 @@ class MergeMixin(ViewSetMixin):
|
||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
|
||||
schema = TreeSchema()
|
||||
model = None
|
||||
|
||||
@ -237,11 +271,13 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
except self.model.DoesNotExist:
|
||||
self.queryset = self.model.objects.none()
|
||||
else:
|
||||
return super().get_queryset()
|
||||
return self.queryset.filter(space=self.request.space).order_by('name')
|
||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by('name')
|
||||
|
||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
||||
|
||||
@ decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||
@ decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
def move(self, request, pk, parent):
|
||||
self.description = f"Move {self.basename} to be a child of {self.basename} with ID of [int]. Use ID: 0 to move {self.basename} to the root."
|
||||
if self.model.node_order_by:
|
||||
@ -364,7 +400,7 @@ class SupermarketCategoryViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
permission_classes = [CustomIsUser]
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by('name')
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@ -413,7 +449,15 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
permission_classes = [CustomIsUser]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer,)
|
||||
def get_queryset(self):
|
||||
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [self.request.user.id]
|
||||
|
||||
self.queryset = super().get_queryset()
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
|
||||
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
||||
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users', 'inherit_fields').select_related('recipe', 'supermarket_category')
|
||||
|
||||
@ decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer,)
|
||||
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
|
||||
def shopping(self, request, pk):
|
||||
if self.request.space.demo:
|
||||
@ -561,7 +605,9 @@ class RecipePagination(PageNumberPagination):
|
||||
max_page_size = 100
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
self.facets = get_facet(qs=queryset, request=request)
|
||||
if queryset is None:
|
||||
raise Exception
|
||||
self.facets = RecipeFacet(request, queryset=queryset)
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
@ -570,7 +616,7 @@ class RecipePagination(PageNumberPagination):
|
||||
('next', self.get_next_link()),
|
||||
('previous', self.get_previous_link()),
|
||||
('results', data),
|
||||
('facets', self.facets)
|
||||
('facets', self.facets.get_facets(from_cache=True))
|
||||
]))
|
||||
|
||||
|
||||
@ -607,9 +653,11 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
if not (share and self.detail):
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
|
||||
self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
|
||||
|
||||
return super().get_queryset()
|
||||
# self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
|
||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
||||
search = RecipeSearch(self.request, **params)
|
||||
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
|
||||
return self.queryset
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if self.request.GET.get('debug', False):
|
||||
@ -1051,7 +1099,7 @@ def recipe_from_source(request):
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': True,
|
||||
'msg': _('No useable data could be found.')
|
||||
'msg': _('No usable data could be found.')
|
||||
},
|
||||
status=400
|
||||
)
|
||||
@ -1100,10 +1148,20 @@ def ingredient_from_string(request):
|
||||
@group_required('user')
|
||||
def get_facets(request):
|
||||
key = request.GET.get('hash', None)
|
||||
food = request.GET.get('food', None)
|
||||
keyword = request.GET.get('keyword', None)
|
||||
facets = RecipeFacet(request, hash_key=key)
|
||||
|
||||
if food:
|
||||
results = facets.add_food_children(food)
|
||||
elif keyword:
|
||||
results = facets.add_keyword_children(keyword)
|
||||
else:
|
||||
results = facets.get_facets()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
'facets': get_facet(request=request, use_cache=False, hash_key=key),
|
||||
'facets': results,
|
||||
},
|
||||
status=200
|
||||
)
|
||||
|
@ -45,21 +45,15 @@ def hook(request, token):
|
||||
tb.save()
|
||||
|
||||
if tb.chat_id == str(data['message']['chat']['id']):
|
||||
sl = ShoppingList.objects.filter(Q(created_by=tb.created_by)).filter(finished=False, space=tb.space).order_by('-created_at').first()
|
||||
if not sl:
|
||||
sl = ShoppingList.objects.create(created_by=tb.created_by, space=tb.space)
|
||||
|
||||
request.space = tb.space # TODO this is likely a bad idea. Verify and test
|
||||
request.user = tb.created_by
|
||||
ingredient_parser = IngredientParser(request, False)
|
||||
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text'])
|
||||
f = ingredient_parser.get_food(ingredient)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
sl.entries.add(
|
||||
ShoppingListEntry.objects.create(
|
||||
food=f, unit=u, amount=amount
|
||||
)
|
||||
)
|
||||
|
||||
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
|
||||
|
||||
return JsonResponse({'data': data['message']['text']})
|
||||
except Exception:
|
||||
pass
|
||||
|
@ -411,7 +411,7 @@ def user_settings(request):
|
||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||
api_token = Token.objects.create(user=request.user)
|
||||
|
||||
# these fields require postgress - just disable them if postgress isn't available
|
||||
# these fields require postgresql - just disable them if postgresql isn't available
|
||||
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']:
|
||||
search_form.fields['search'].disabled = True
|
||||
@ -567,6 +567,8 @@ def space(request):
|
||||
form = SpacePreferenceForm(request.POST, prefix='space')
|
||||
if form.is_valid():
|
||||
request.space.food_inherit.set(form.cleaned_data['food_inherit'])
|
||||
request.space.show_facet_count = form.cleaned_data['show_facet_count']
|
||||
request.space.save()
|
||||
if form.cleaned_data['reset_food_inherit']:
|
||||
Food.reset_inheritance(space=request.space)
|
||||
|
||||
|
43
docs/faq.md
43
docs/faq.md
@ -2,7 +2,23 @@ There are several questions and issues that come up from time to time. Here are
|
||||
Please note that the existence of some questions is due the application not being perfect in some parts.
|
||||
Many of those shortcomings are planned to be fixed in future release but simply could not be addressed yet due to time limits.
|
||||
|
||||
## CSRF Errors
|
||||
## Is there a Tandoor app?
|
||||
Tandoor can be installed as a progressive web app (PWA) on mobile and desktop devices. The PWA stores recently accessed recipes locally for offline use.
|
||||
|
||||
### Mobile browsers
|
||||
|
||||
#### Safari (iPhone/iPad)
|
||||
Open Tandoor, click Safari's share button, select `Add to Home Screen`
|
||||
|
||||
### Desktop browsers
|
||||
|
||||
#### Google Chrome
|
||||
Open Tandoor, open the menu behind the three vertical dots at the top right, select `Install Tandoor Recipes...`
|
||||
|
||||
#### Microsoft Edge
|
||||
Open Tandoor, open the menu behind the three horizontal dots at the top right, select `Apps > Install Tandoor Recipes`
|
||||
|
||||
## Why am I getting CSRF Errors?
|
||||
If you are getting CSRF Errors this is most likely due to a reverse proxy not passing the correct headers.
|
||||
|
||||
If you are using swag by linuxserver you might need `proxy_set_header X-Forwarded-Proto $scheme;` in your nginx config.
|
||||
@ -10,15 +26,15 @@ If you are using a plain ngix you might need `proxy_set_header Host $http_host;`
|
||||
|
||||
Further discussions can be found in this [Issue #518](https://github.com/vabene1111/recipes/issues/518)
|
||||
|
||||
## Images not loading
|
||||
If images are not loading this might be related to the same issue as the CSRF Errors.
|
||||
A discussion about that can be found [Issue #452](https://github.com/vabene1111/recipes/issues/452)
|
||||
## Why are images not loading?
|
||||
If images are not loading this might be related to the same issue as the CSRF errors (see above).
|
||||
A discussion about that can be found at [Issue #452](https://github.com/vabene1111/recipes/issues/452)
|
||||
|
||||
The other common issue is that the recommended nginx container is removed from the deployment stack.
|
||||
If removed, the nginx webserver needs to be replaced by something else that servers the /mediafiles/ directory or
|
||||
`GUNICORN_MEDIA` needs to be enabled to allow media serving by the application container itself.
|
||||
|
||||
## User Creation
|
||||
## How can I create users?
|
||||
To create a new user click on your name (top right corner) and select system. There click on invite links and create a new invite link.
|
||||
|
||||
It is not possible to create users through the admin because users must be assigned a default group and space.
|
||||
@ -28,22 +44,27 @@ To change a users space you need to go to the admin and select User Infos.
|
||||
If you use an external auth provider or proxy authentication make sure to specify a default group and space in the
|
||||
environment configuration.
|
||||
|
||||
## Spaces
|
||||
## What are spaces?
|
||||
Spaces are a feature used to separate one installation of Tandoor into several parts.
|
||||
In technical terms it is a multi tenant system.
|
||||
|
||||
You can compare a space to something like google drive or dropbox.
|
||||
There is only one installation of the Dropbox system, but it handles multiple users without them noticing each other.
|
||||
For Tandoor that means all people that work together on one recipe collection can be in one space.
|
||||
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (trough the admin interface).
|
||||
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (through the admin interface).
|
||||
|
||||
Sharing between spaces is currently not possible but is planned for future releases.
|
||||
|
||||
## Create Admin user / reset passwords
|
||||
To create a superuser or reset a lost password if access to the container is lost you need to
|
||||
## How can I reset passwords?
|
||||
To reset a lost password if access to the container is lost you need to
|
||||
|
||||
1. execute into the container using `docker-compose exec web_recipes sh`
|
||||
2. activate the virtual environment `source venv/bin/activate`
|
||||
3. run `python manage.py createsuperuser` and follow the steps shown.
|
||||
3. run `python manage.py changepassword <username>` and follow the steps shown.
|
||||
|
||||
To change a password enter `python manage.py changepassword <username>` in step 3.
|
||||
## How can I add an admin user?
|
||||
To create a superuser you need to
|
||||
|
||||
1. execute into the container using `docker-compose exec web_recipes sh`
|
||||
2. activate the virtual environment `source venv/bin/activate`
|
||||
3. run `python manage.py createsuperuser` and follow the steps shown.
|
@ -25,14 +25,14 @@ SOCIAL_PROVIDERS=allauth.socialaccount.providers.github,allauth.socialaccount.pr
|
||||
The exact formatting is important so make sure to follow the steps explained here!
|
||||
|
||||
Depending on your authentication provider you **might need** to configure it.
|
||||
This needs to be done trough the settings system. To make the system flexible (allow multiple providers) and to
|
||||
not require another file to be mounted into the container the configuration ins done trough a single
|
||||
This needs to be done through the settings system. To make the system flexible (allow multiple providers) and to
|
||||
not require another file to be mounted into the container the configuration ins done through a single
|
||||
environment variable. The downside of this approach is that the configuration needs to be put into a single line
|
||||
as environment files loaded by docker compose don't support multiple lines for a single variable.
|
||||
|
||||
Take the example configuration from the allauth docs, fill in your settings and then inline the whole object
|
||||
(you can use a service like [www.freeformatter.com](https://www.freeformatter.com/json-formatter.html) for formatting).
|
||||
Assign it to the `SOCIALACCOUNT_PROVIDERS` variable.
|
||||
Assign it to the additional `SOCIALACCOUNT_PROVIDERS` variable.
|
||||
|
||||
```ini
|
||||
SOCIALACCOUNT_PROVIDERS={"nextcloud":{"SERVER":"https://nextcloud.example.org"}}
|
||||
@ -56,6 +56,25 @@ Use the superuser account to grant permissions to the newly created users.
|
||||
I do not have a ton of experience with using various single signon providers and also cannot test all of them.
|
||||
If you have any Feedback or issues let me know.
|
||||
|
||||
### Third-party authentication example
|
||||
Keycloak is a popular IAM solution and integration is straight forward thanks to Django Allauth. This example can also be used as reference for other third-party authentication solutions, as documented by Allauth.
|
||||
|
||||
At Keycloak, create a new client and assign a `Client-ID`, this client comes with a `Secret-Key`. Both values are required later on. Make sure to define the correct Redirection-URL for the service, for example `https://tandoor.example.com/*`. Depending on your Keycloak setup, you need to assign roles and groups to grant access to the service.
|
||||
|
||||
To enable Keycloak as a sign in option, set those variables to define the social provider and specify its configuration:
|
||||
```ini
|
||||
SOCIAL_PROVIDERS=allauth.socialaccount.providers.keycloak
|
||||
SOCIALACCOUNT_PROVIDERS='{ "keycloak": { "KEYCLOAK_URL": "https://auth.example.com/", "KEYCLOAK_REALM": "master" } }'
|
||||
```
|
||||
|
||||
1. Restart the service, login as superuser and open the `Admin` page.
|
||||
2. Make sure that the correct `Domain Name` is defined at `Sites`.
|
||||
3. Select `Social Application` and chose `Keycloak` from the provider list.
|
||||
4. Provide an arbitrary name for your authentication provider, and enter the `Client-ID` and `Secret Key` values obtained from Keycloak earlier.
|
||||
5. Make sure to add your `Site` to the list of available sites and save the new `Social Application`.
|
||||
|
||||
You are now able to sign in using Keycloak.
|
||||
|
||||
### Linking accounts
|
||||
To link an account to an already existing normal user go to the settings page of the user and link it.
|
||||
Here you can also unlink your account if you no longer want to use a social login method.
|
||||
|
@ -35,7 +35,7 @@ The basic configuration is the same for all providers.
|
||||
### Local
|
||||
|
||||
!!! info
|
||||
There is currently no way to upload files trough the webinterface. This is a feature that might be added later.
|
||||
There is currently no way to upload files through the webinterface. This is a feature that might be added later.
|
||||
|
||||
The local provider does not need any configuration.
|
||||
For the monitor you will need to define a valid path on your host system.
|
||||
|
@ -9,7 +9,7 @@
|
||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continuous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||
@ -85,7 +85,7 @@ there are some greater overall goals for the future (in no particular order)
|
||||
|
||||
- Improve the UI! The Design is inconsistent and many pages work but don't look great. This needs to change.
|
||||
- I strongly believe in Open Data and Systems. Thus adding importers and exporters for all relevant other recipe management systems is something i really want to do.
|
||||
- Move all Javascript Libraries to a packet manger and clean up some of the mess I made in the early days
|
||||
- Move all Javascript Libraries to a packet manager and clean up some of the mess I made in the early days
|
||||
- Improve Test coverage and also the individual tests themselves
|
||||
- Improve the documentation for all features and aspects of this project and add some application integrated help
|
||||
|
||||
|
@ -60,6 +60,7 @@ The main, and also recommended, installation option is to install this applicati
|
||||
### Plain
|
||||
|
||||
This configuration exposes the application through an nginx web server on port 80 of your machine.
|
||||
Be aware that having some other web server or container running on your host machine on port 80 will block this from working.
|
||||
|
||||
```shell
|
||||
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml
|
||||
@ -73,6 +74,8 @@ wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/d
|
||||
|
||||
Most deployments will likely use a reverse proxy.
|
||||
|
||||
If your reverse proxy is not listed here, please refer to [Others](https://docs.tandoor.dev/install/docker/#others).
|
||||
|
||||
#### Traefik
|
||||
|
||||
If you use traefik, this configuration is the one for you.
|
||||
@ -135,27 +138,13 @@ In both cases, also make sure to mount `/media/` in your swag container to point
|
||||
|
||||
Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup.
|
||||
|
||||
#### Nginx Swag by LinuxServer
|
||||
For step-by-step instructions to set this up from scratch, see [this example](swag.md).
|
||||
|
||||
[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io
|
||||
### Others
|
||||
|
||||
It also contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance
|
||||
If you use none of the above mentioned reverse proxies or want to use an existing one on your host machine (like a local nginx or Caddy), simply use the [PLAIN](https://docs.tandoor.dev/install/docker/#plain) setup above and change the outbound port to one of your liking.
|
||||
|
||||
If you're running Swag on the default port, you'll just need to change the container name to yours.
|
||||
|
||||
If your running Swag on a custom port, some headers must be changed. To do this,
|
||||
|
||||
- Create a copy of `proxy.conf`
|
||||
- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to
|
||||
- `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;`
|
||||
- Update `recipes.subdomain.conf` to use the new file
|
||||
- Restart the linuxserver/swag container and Recipes will work
|
||||
|
||||
More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627).
|
||||
|
||||
In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory.
|
||||
|
||||
Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup.
|
||||
An example port config (inside the respective docker-compose.yml) would be: `8123:80` instead of the `80:80` or if you want to be sure, that Tandoor is **just** accessible via your proxy and don't wanna bother with your firewall, then `127.0.0.1:8123:80` is a viable option too.
|
||||
|
||||
## Additional Information
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# K8s Setup
|
||||
|
||||
This is a setup which should be sufficent for production use. Be sure to replace the default secrets!
|
||||
This is a setup which should be sufficient for production use. Be sure to replace the default secrets!
|
||||
|
||||
# Files
|
||||
|
||||
@ -45,7 +45,7 @@ The creation of the persistent volume claims for media and static content. May y
|
||||
|
||||
## 40-sts-postgresql.yaml
|
||||
|
||||
The PostgreSQL stateful set, based on a bitnami image. It runs a init container as root to do the preparations. The postgres container itsef runs as a lower privileged user. The recipes app uses the database super user (postgres) as the recipies app is doing some db migrations on startup, which needs super user privileges.
|
||||
The PostgreSQL stateful set, based on a bitnami image. It runs a init container as root to do the preparations. The postgres container itself runs as a lower privileged user. The recipes app uses the database super user (postgres) as the recipes app is doing some db migrations on startup, which needs super user privileges.
|
||||
|
||||
## 45-service-db.yaml
|
||||
|
||||
@ -53,7 +53,7 @@ Creating the database service.
|
||||
|
||||
## 50-deployment.yaml
|
||||
|
||||
The deployment first fires up a init container to do the database migrations and file modifications. This init container runs as root. The init conainer runs part of the [boot.sh](https://github.com/TandoorRecipes/recipes/blob/develop/boot.sh) script from the `vabene1111/recipes` image.
|
||||
The deployment first fires up a init container to do the database migrations and file modifications. This init container runs as root. The init container runs part of the [boot.sh](https://github.com/TandoorRecipes/recipes/blob/develop/boot.sh) script from the `vabene1111/recipes` image.
|
||||
|
||||
The deployment then runs two containers, the recipes-nginx and the recipes container which runs the gunicorn app. The nginx container gets it's nginx.conf via config map to deliver static content `/static` and `/media`. The guincorn container gets it's secret key and the database password from the secret `recipes`. `gunicorn` runs as user `nobody`.
|
||||
|
||||
@ -94,5 +94,5 @@ These manifests are tested against Release 1.0.1. Newer versions may not work wi
|
||||
To apply the manifest with kubectl, use the following command:
|
||||
|
||||
~~~
|
||||
kubectl apply -f ./docs/k8s/
|
||||
kubectl apply -f ./docs/install/k8s/
|
||||
~~~
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Manual installation instructions
|
||||
|
||||
These intructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04))
|
||||
These instructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04))
|
||||
|
||||
!!! warning
|
||||
Be sure to use python 3.9 and pip related to python 3.9. Depending on your distribution calling `python` or `pip` will use python2 instead of python 3.9.
|
||||
@ -115,7 +115,7 @@ ExecStart=/var/www/recipes/bin/gunicorn --error-logfile /tmp/gunicorn_err.log --
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
*Note*: `-error-logfile /tmp/gunicorn_err.log --log-level debug --capture-output` are usefull for debugging and can be removed later
|
||||
*Note*: `-error-logfile /tmp/gunicorn_err.log --log-level debug --capture-output` are useful for debugging and can be removed later
|
||||
|
||||
*Note2*: Fix the path in the `ExecStart` line to where you gunicorn and recipes are
|
||||
|
||||
@ -134,11 +134,11 @@ server {
|
||||
#error_log /var/log/nginx/error.log;
|
||||
|
||||
# serve media files
|
||||
location /staticfiles {
|
||||
location /static {
|
||||
alias /var/www/recipes/staticfiles;
|
||||
}
|
||||
|
||||
location /mediafiles {
|
||||
location /media {
|
||||
alias /var/www/recipes/mediafiles;
|
||||
}
|
||||
|
||||
|
@ -57,4 +57,4 @@ apache:
|
||||
|
||||
I used two paths `<sub path>` and `<www path>` for simplicity. In my case I have `<sub path> = recipes` and `<www path> = serve/recipes`. One could also change the matching rules of traefik to have everything under one path.
|
||||
|
||||
I left out the TLS config in this example for simplicty.
|
||||
I left out the TLS config in this example for simplicity.
|
||||
|
118
docs/install/swag.md
Normal file
118
docs/install/swag.md
Normal file
@ -0,0 +1,118 @@
|
||||
!!! danger
|
||||
Please refer to the [official documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup. This example shows just one setup that may or may not differ from yours in significant ways. This tutorial does not cover security measures, backups, and many other things that you might want to consider.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You have a newly spun-up Ubuntu server with docker (pre-)installed.
|
||||
- At least one `mydomain.com` and one `mysubdomain.mydomain.com` are pointing to the server's IP. (This tutorial does not cover subfolder installation.)
|
||||
- You have an ssh terminal session open.
|
||||
|
||||
## Installation
|
||||
|
||||
### Download and edit Tandoor configuration
|
||||
|
||||
```
|
||||
cd /opt
|
||||
mkdir recipes
|
||||
cd recipes
|
||||
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env
|
||||
base64 /dev/urandom | head -c50
|
||||
```
|
||||
Copy the response from that last command and paste the key into the `.env` file:
|
||||
```
|
||||
nano .env
|
||||
```
|
||||
You'll also need to enter a Postgres password into the `.env` file. Then, save the file and exit the editor.
|
||||
|
||||
### Install and configure Docker Compose
|
||||
|
||||
In keeping with [these instructions](https://docs.linuxserver.io/general/docker-compose):
|
||||
```
|
||||
cd /opt
|
||||
curl -L --fail https://raw.githubusercontent.com/linuxserver/docker-docker-compose/master/run.sh -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
Next, create and edit the docker compose file.
|
||||
|
||||
```
|
||||
nano docker-compose.yml
|
||||
```
|
||||
|
||||
Paste the following and adjust your domains, subdomains and time zone.
|
||||
|
||||
```
|
||||
---
|
||||
version: "2.1"
|
||||
services:
|
||||
swag:
|
||||
image: ghcr.io/linuxserver/swag
|
||||
container_name: swag
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Berlin # <---- EDIT THIS <---- <----
|
||||
- URL=mydomain.com # <---- EDIT THIS <---- <----
|
||||
- SUBDOMAINS=mysubdomain,myothersubdomain # <---- EDIT THIS <---- <----
|
||||
- EXTRA_DOMAINS=myotherdomain.com # <---- EDIT THIS <---- <----
|
||||
- VALIDATION=http
|
||||
volumes:
|
||||
- ./swag:/config
|
||||
- ./recipes/media:/media
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80
|
||||
restart: unless-stopped
|
||||
|
||||
db_recipes:
|
||||
restart: always
|
||||
container_name: db_recipes
|
||||
image: postgres:11-alpine
|
||||
volumes:
|
||||
- ./recipes/db:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./recipes/.env
|
||||
|
||||
recipes:
|
||||
image: vabene1111/recipes
|
||||
container_name: recipes
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./recipes/.env
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
- TZ=Europe/Berlin # <---- EDIT THIS <---- <----
|
||||
volumes:
|
||||
- ./recipes/static:/opt/recipes/staticfiles
|
||||
- ./recipes/media:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
- db_recipes
|
||||
```
|
||||
|
||||
Save and exit.
|
||||
|
||||
### Create containers and configure swag reverse proxy
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
```
|
||||
cd /opt/swag/nginx/proxy-confs
|
||||
cp recipes.subdomain.conf.sample recipes.subdomain.conf
|
||||
nano recipes.subdomain.conf
|
||||
```
|
||||
|
||||
Change the line `server_name recipes.*;` to `server_name mysubdomain.*;`, save and exit.
|
||||
|
||||
### Finalize
|
||||
|
||||
```
|
||||
cd /opt
|
||||
docker restart swag recipes
|
||||
```
|
||||
|
||||
Go to `https://mysubdomain.mydomain.com`. (If you get a "502 Bad Gateway" error, be patient. It might take a short while until it's functional.)
|
@ -78,7 +78,7 @@ Easiest way is to do it via Reverse Proxy
|
||||
- insert name
|
||||
- Source:
|
||||
- Protocol: HTTPS
|
||||
- Hostname: URL if you acces from outside, otherwise ip in network
|
||||
- Hostname: URL if you access from outside, otherwise ip in network
|
||||
- Port: The port you want to access, has to be a different one that the one in the docker-compose file
|
||||
- HSTS can be enabled
|
||||
- Destination:
|
||||
@ -88,14 +88,14 @@ Easiest way is to do it via Reverse Proxy
|
||||
- Click on Custom Header and press Create -> Websocket
|
||||
- Save
|
||||
- Control Panel -> Security -> Firewall -> Edit Rules -> Create
|
||||
- Ports: Select form a list of build-in applications -> Select -> You find your Reverse Proxy, enable it
|
||||
- Ports: Select form a list of built-in applications -> Select -> You find your Reverse Proxy, enable it
|
||||
- Source IP: Depends, All allows access from outside, i use specific to only connect in my network
|
||||
- Action: Allow
|
||||
- Save and make sure it's above the deny rules
|
||||
|
||||
[Deprecated, Note: ssl Path changed for DSM 7]
|
||||
6.1 Additional SSL Setup
|
||||
- create foler `ssl` inside `nginx` folder
|
||||
- create folder `ssl` inside `nginx` folder
|
||||
- download your ssl certificate from `security` tab in dsm `control panel`
|
||||
- or create a task in `task manager` because Synology will update the certificate every few months
|
||||
- set task to repeat every day
|
||||
|
@ -1,6 +1,6 @@
|
||||
!!! danger
|
||||
Please refer to [the offical documentation](https://doc.traefik.io/traefik/).
|
||||
This example just shows something similar to my setup in case you dont understand the offical documentation.
|
||||
Please refer to [the official documentation](https://doc.traefik.io/traefik/).
|
||||
This example just shows something similar to my setup in case you dont understand the official documentation.
|
||||
|
||||
You need to create a network called `traefik` using `docker network create traefik`.
|
||||
## docker-compose.yml
|
||||
|
@ -18,7 +18,7 @@ After that, you can go to the "Apps" tab in unRAID and search for Recipes and lo
|
||||

|
||||
|
||||
The default settings should by fine for most users, just be sure to enter a secret key that is randomly generated.
|
||||
Then chooose apply.
|
||||
Then choose apply.
|
||||
|
||||

|
||||
|
||||
|
@ -2,7 +2,7 @@ There is currently no "good" way of backing up your data implemented in the appl
|
||||
This mean that you will be responsible for backing up your data.
|
||||
|
||||
It is planned to add a "real" backup feature similar to applications like homeassistant where a snapshot can be
|
||||
downloaded and restored trough the web interface.
|
||||
downloaded and restored through the web interface.
|
||||
|
||||
!!! warning
|
||||
When developing a new backup strategy, make sure to also test the restore process!
|
||||
|
@ -21,13 +21,13 @@ The following table roughly defines the capabilities of each role
|
||||
!!! warning
|
||||
Users without groups cannot do anything. Make sure to assign them a group!
|
||||
|
||||
You can either create new users trough the admin interface or by sending them invite links.
|
||||
You can either create new users through the admin interface or by sending them invite links.
|
||||
|
||||
Invite links can be generated on the System page. If you specify a username during the creation of the link
|
||||
the person using it won't be able to change that name.
|
||||
|
||||
## Managing Permissions
|
||||
Management of permissions can currently only be achieved trough the django admin interface.
|
||||
Management of permissions can currently only be achieved through the django admin interface.
|
||||
|
||||
!!! warning
|
||||
Please do not rename the groups as this breaks the permission system.
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,52 +18,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
#, fuzzy
|
||||
#| msgid "English"
|
||||
msgid "Polish"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -17,50 +17,50 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -19,50 +19,50 @@ msgstr ""
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
|
||||
"2);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -17,50 +17,50 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,50 +18,50 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -17,50 +17,50 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:353
|
||||
#: .\recipes\settings.py:357
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:354
|
||||
#: .\recipes\settings.py:358
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:355
|
||||
#: .\recipes\settings.py:359
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:356
|
||||
#: .\recipes\settings.py:360
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:357
|
||||
#: .\recipes\settings.py:361
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:358
|
||||
#: .\recipes\settings.py:362
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:359
|
||||
#: .\recipes\settings.py:363
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:360
|
||||
#: .\recipes\settings.py:364
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:361
|
||||
#: .\recipes\settings.py:365
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:362
|
||||
#: .\recipes\settings.py:366
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:363
|
||||
#: .\recipes\settings.py:367
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:364
|
||||
#: .\recipes\settings.py:368
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -1,3 +1,4 @@
|
||||
import time
|
||||
from os import getenv
|
||||
|
||||
from django.conf import settings
|
||||
@ -13,19 +14,20 @@ class CustomRemoteUser(RemoteUserMiddleware):
|
||||
Gist code by vstoykov, you can check his original gist at:
|
||||
https://gist.github.com/vstoykov/1390853/5d2e8fac3ca2b2ada8c7de2fb70c021e50927375
|
||||
Changes:
|
||||
Ignoring static file requests and a certain useless admin request from triggering the logger.
|
||||
Ignoring static file requests and a certain useless admin request from triggering the logger.
|
||||
Updated statements to make it Python 3 friendly.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def terminal_width():
|
||||
"""
|
||||
Function to compute the terminal width.
|
||||
"""
|
||||
width = 0
|
||||
try:
|
||||
import struct, fcntl, termios
|
||||
import fcntl
|
||||
import struct
|
||||
import termios
|
||||
s = struct.pack('HHHH', 0, 0, 0, 0)
|
||||
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
|
||||
width = struct.unpack('HHHH', x)[1]
|
||||
|
@ -259,7 +259,7 @@ WSGI_APPLICATION = 'recipes.wsgi.application'
|
||||
# Load settings from env files
|
||||
if os.getenv('DATABASE_URL'):
|
||||
match = re.match(
|
||||
r'(?P<schema>\w+):\/\/(?P<user>[\w\d_-]+)(:(?P<password>[^@]+))?@(?P<host>[^:/]+)(:(?P<port>\d+))?(\/(?P<database>[\w\d_-]+))?',
|
||||
r'(?P<schema>\w+):\/\/(?P<user>[\w\d_-]+)(:(?P<password>[^@]+))?@(?P<host>[^:/]+)(:(?P<port>\d+))?(\/(?P<database>[\w\d\/\._-]+))?',
|
||||
os.getenv('DATABASE_URL')
|
||||
)
|
||||
settings = match.groupdict()
|
||||
@ -372,10 +372,10 @@ LANGUAGES = [
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||||
|
||||
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
|
||||
# path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest
|
||||
JS_REVERSE_OUTPUT_PATH = os.path.join(BASE_DIR, "cookbook/static/django_js_reverse")
|
||||
|
||||
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', os.getenv('SCRIPT_NAME', ''))
|
||||
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
|
||||
|
||||
STATIC_URL = os.getenv('STATIC_URL', '/static/')
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
||||
@ -408,7 +408,7 @@ TEST_RUNNER = "cookbook.helper.CustomTestRunner.CustomTestRunner"
|
||||
# settings for cross site origin (CORS)
|
||||
# all origins allowed to support bookmarklet
|
||||
# all of this may or may not work with nginx or other web servers
|
||||
# TODO make this user configureable - enable or disable bookmarklets
|
||||
# TODO make this user configurable - enable or disable bookmarklets
|
||||
# TODO since token auth is enabled - this all should be https by default
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
|
@ -54,14 +54,20 @@
|
||||
<div class="col-12 col-md-3 calender-options">
|
||||
<h5>{{ $t("Planner_Settings") }}</h5>
|
||||
<b-form>
|
||||
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')" label-for="UomInput">
|
||||
<b-form-select id="UomInput" v-model="settings.displayPeriodUom" :options="options.displayPeriodUom"></b-form-select>
|
||||
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')"
|
||||
label-for="UomInput">
|
||||
<b-form-select id="UomInput" v-model="settings.displayPeriodUom"
|
||||
:options="options.displayPeriodUom"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group id="PeriodInput" :label="$t('Periods')" :description="$t('Plan_Show_How_Many_Periods')" label-for="PeriodInput">
|
||||
<b-form-select id="PeriodInput" v-model="settings.displayPeriodCount" :options="options.displayPeriodCount"></b-form-select>
|
||||
<b-form-group id="PeriodInput" :label="$t('Periods')"
|
||||
:description="$t('Plan_Show_How_Many_Periods')" label-for="PeriodInput">
|
||||
<b-form-select id="PeriodInput" v-model="settings.displayPeriodCount"
|
||||
:options="options.displayPeriodCount"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')" label-for="DaysInput">
|
||||
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek" :options="dayNames"></b-form-select>
|
||||
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')"
|
||||
label-for="DaysInput">
|
||||
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek"
|
||||
:options="dayNames"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group id="WeekNumInput" :label="$t('Week_Numbers')">
|
||||
<b-form-checkbox v-model="settings.displayWeekNumbers" name="week_num">
|
||||
@ -73,19 +79,24 @@
|
||||
<div class="col-12 col-md-9 col-lg-6">
|
||||
<h5>{{ $t("Meal_Types") }}</h5>
|
||||
<div>
|
||||
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" handle=".handle" @sort="sortMealTypes()">
|
||||
<b-card no-body class="mt-1" v-for="(meal_type, index) in meal_types" v-hover :key="meal_type.id">
|
||||
<b-card-header class="p-4">
|
||||
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" @sort="sortMealTypes()" ghost-class="ghost">
|
||||
<b-card no-body class="mt-1 list-group-item p-2" style="cursor:move" v-for="(meal_type, index) in meal_types" v-hover
|
||||
:key="meal_type.id">
|
||||
<b-card-header class="p-2 border-0">
|
||||
<div class="row">
|
||||
<div class="col-2 handle">
|
||||
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-lg shadow-none"><i
|
||||
class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<h5>
|
||||
{{ meal_type.icon }} {{ meal_type.name
|
||||
}}<span class="float-right text-primary"
|
||||
><i class="fa" v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }" @click="editOrSaveMealType(index)" aria-hidden="true"></i
|
||||
></span>
|
||||
<h5 class="mt-1 mb-1">
|
||||
{{ meal_type.icon }} {{
|
||||
meal_type.name
|
||||
}}<span class="float-right text-primary" style="cursor:pointer"
|
||||
><i class="fa"
|
||||
v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }"
|
||||
@click="editOrSaveMealType(index)" aria-hidden="true"></i
|
||||
></span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
@ -93,20 +104,29 @@
|
||||
<b-card-body class="p-4" v-if="meal_type.editing">
|
||||
<div class="form-group">
|
||||
<label>{{ $t("Name") }}</label>
|
||||
<input class="form-control" placeholder="Name" v-model="meal_type.name" />
|
||||
<input class="form-control" placeholder="Name" v-model="meal_type.name"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<emoji-input :field="'icon'" :label="$t('Icon')" :value="meal_type.icon"></emoji-input>
|
||||
<emoji-input :field="'icon'" :label="$t('Icon')"
|
||||
:value="meal_type.icon"></emoji-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t("Color") }}</label>
|
||||
<input class="form-control" type="color" name="Name" :value="meal_type.color" @change="meal_type.color = $event.target.value" />
|
||||
<input class="form-control" type="color" name="Name"
|
||||
:value="meal_type.color"
|
||||
@change="meal_type.color = $event.target.value"/>
|
||||
</div>
|
||||
<b-form-checkbox id="checkbox-1" v-model="meal_type.default" name="default_checkbox" class="mb-2">
|
||||
<b-form-checkbox id="checkbox-1" v-model="meal_type.default"
|
||||
name="default_checkbox" class="mb-2">
|
||||
{{ $t("Default") }}
|
||||
</b-form-checkbox>
|
||||
<button class="btn btn-danger" @click="deleteMealType(index)">{{ $t("Delete") }}</button>
|
||||
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">{{ $t("Save") }}</button>
|
||||
<button class="btn btn-danger" @click="deleteMealType(index)">{{
|
||||
$t("Delete")
|
||||
}}
|
||||
</button>
|
||||
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</draggable>
|
||||
@ -127,7 +147,15 @@
|
||||
openEntryEdit(contextData.originalItem.entry)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{ $t("Edit") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{
|
||||
$t("Edit")
|
||||
}}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="contextData.originalItem.entry.recipe != null"
|
||||
@click="$refs.menu.close();openRecipe(contextData.originalItem.entry.recipe)">
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pizza-slice"></i>
|
||||
{{ $t("Recipe") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@ -135,7 +163,8 @@
|
||||
moveEntryLeft(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i> {{ $t("Move") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i>
|
||||
{{ $t("Move") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@ -143,7 +172,8 @@
|
||||
moveEntryRight(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i> {{ $t("Move") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i>
|
||||
{{ $t("Move") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@ -159,7 +189,8 @@
|
||||
addToShopping(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i>
|
||||
{{ $t("Add_to_Shopping") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@ -167,7 +198,8 @@
|
||||
deleteEntry(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i>
|
||||
{{ $t("Delete") }}</a>
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
@ -198,10 +230,12 @@
|
||||
<div class="col-12 mt-1" v-if="shopping_list.length > 0">
|
||||
<b-button-group>
|
||||
<b-button variant="success" @click="saveShoppingList"
|
||||
><i class="fas fa-external-link-alt"></i>
|
||||
><i class="fas fa-external-link-alt"></i>
|
||||
{{ $t("Open") }}
|
||||
</b-button>
|
||||
<b-button variant="danger" @click="shopping_list = []"><i class="fa fa-trash"></i> {{ $t("Clear") }} </b-button>
|
||||
<b-button variant="danger" @click="shopping_list = []"><i class="fa fa-trash"></i>
|
||||
{{ $t("Clear") }}
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,37 +243,46 @@
|
||||
</div>
|
||||
</template>
|
||||
<transition name="slide-fade">
|
||||
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
||||
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)"
|
||||
v-if="current_tab === 0">
|
||||
<div class="col-md-3 col-6">
|
||||
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i class="fas fa-calendar-plus"></i> {{ $t("Create") }}</button>
|
||||
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
|
||||
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i class="fas fa-shopping-cart"></i> {{ $t("Shopping_list") }}</button>
|
||||
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i
|
||||
class="fas fa-shopping-cart"></i> {{ $t("Shopping_list") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
|
||||
><i class="fas fa-download"></i>
|
||||
><i class="fas fa-download"></i>
|
||||
{{ $t("Export_To_ICal") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top :title="$t('Coming_Soon')">
|
||||
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top
|
||||
:title="$t('Coming_Soon')">
|
||||
{{ $t("Auto_Planner") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
|
||||
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
|
||||
<b-button-group class="mx-1">
|
||||
<b-button v-html="'<<'" @click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
|
||||
<b-button v-html="'<<'"
|
||||
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
|
||||
<b-button v-html="'<'" @click="setStartingDay(-1)"></b-button>
|
||||
</b-button-group>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i class="fas fa-home"></i> </b-button>
|
||||
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
|
||||
class="fas fa-home"></i></b-button>
|
||||
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
|
||||
</b-button-group>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button v-html="'>'" @click="setStartingDay(1)"></b-button>
|
||||
<b-button v-html="'>>'" @click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
|
||||
<b-button v-html="'>>'"
|
||||
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
|
||||
</b-button-group>
|
||||
</b-button-toolbar>
|
||||
</div>
|
||||
@ -250,7 +293,7 @@
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||
@ -264,11 +307,11 @@ import moment from "moment"
|
||||
import draggable from "vuedraggable"
|
||||
import VueCookies from "vue-cookies"
|
||||
|
||||
import { ApiMixin, StandardToasts } from "@/utils/utils"
|
||||
import { CalendarView, CalendarMathMixin } from "vue-simple-calendar/src/components/bundle"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import {ApiMixin, StandardToasts, ResolveUrlMixin} from "@/utils/utils"
|
||||
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
|
||||
const { makeToast } = require("@/utils/utils")
|
||||
const {makeToast} = require("@/utils/utils")
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
Vue.use(BootstrapVue)
|
||||
@ -288,7 +331,7 @@ export default {
|
||||
EmojiInput,
|
||||
draggable,
|
||||
},
|
||||
mixins: [CalendarMathMixin, ApiMixin],
|
||||
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
|
||||
data: function () {
|
||||
return {
|
||||
showDate: new Date(),
|
||||
@ -306,12 +349,12 @@ export default {
|
||||
current_context_menu_item: null,
|
||||
options: {
|
||||
displayPeriodUom: [
|
||||
{ text: this.$t("Week"), value: "week" },
|
||||
{text: this.$t("Week"), value: "week"},
|
||||
{
|
||||
text: this.$t("Month"),
|
||||
value: "month",
|
||||
},
|
||||
{ text: this.$t("Year"), value: "year" },
|
||||
{text: this.$t("Year"), value: "year"},
|
||||
],
|
||||
displayPeriodCount: [1, 2, 3],
|
||||
entryEditing: {
|
||||
@ -369,7 +412,7 @@ export default {
|
||||
dayNames: function () {
|
||||
let options = []
|
||||
this.getFormattedWeekdayNames(this.userLocale, "long", 0).forEach((day, index) => {
|
||||
options.push({ text: day, value: index })
|
||||
options.push({text: day, value: index})
|
||||
})
|
||||
return options
|
||||
},
|
||||
@ -411,6 +454,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openRecipe: function (recipe) {
|
||||
window.open(this.resolveDjangoUrl('view_recipe', recipe.id))
|
||||
},
|
||||
addToShopping(entry) {
|
||||
if (entry.originalItem.entry.recipe !== null) {
|
||||
this.shopping_list.push(entry.originalItem.entry)
|
||||
@ -445,7 +491,7 @@ export default {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient
|
||||
.createMealType({ name: this.$t("Meal_Type") })
|
||||
.createMealType({name: this.$t("Meal_Type")})
|
||||
.then((e) => {
|
||||
this.periodChangedCallback(this.current_period)
|
||||
})
|
||||
@ -831,4 +877,9 @@ having to override as much.
|
||||
.theme-default .cv-day.draghover {
|
||||
box-shadow: inset 0 0 0.2em 0.2em yellow;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
</style>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<h3>
|
||||
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
||||
<model-menu />
|
||||
<span>{{ this.this_model.name }}</span>
|
||||
<span>{{ $t(this.this_model.name) }}</span>
|
||||
<span v-if="apiName !== 'Step'">
|
||||
<b-button variant="link" @click="startAction({ action: 'new' })">
|
||||
<i class="fas fa-plus-circle fa-2x"></i>
|
||||
@ -242,26 +242,6 @@ export default {
|
||||
let results = result.data?.results ?? result.data
|
||||
|
||||
if (results?.length) {
|
||||
// let secondaryRequest = undefined;
|
||||
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
|
||||
// // the item list is smaller than it should be based on the site the user is own
|
||||
// // this happens when an item is deleted (or merged)
|
||||
// // to prevent issues insert the last item of the previous search page before loading the new results
|
||||
// params.page = params.page - 1
|
||||
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||
// let prev_page_results = result.data?.results ?? result.data
|
||||
// if (prev_page_results?.length) {
|
||||
// results = [prev_page_results[prev_page_results.length]].concat(results)
|
||||
//
|
||||
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
|
||||
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
// this[column + '_counts']['max'] = result.data?.count ?? 0
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
|
||||
this["items_" + column] = this["items_" + column].concat(results)
|
||||
this[column + "_counts"]["current"] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
this[column + "_counts"]["max"] = result.data?.count ?? 0
|
||||
@ -280,11 +260,32 @@ export default {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, { id: id })
|
||||
},
|
||||
saveThis: function (item) {
|
||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [item].concat(this.destroyCard(item?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{ ...item }].concat(this.destroyCard(item?.id, this.items_right))
|
||||
if (!item?.id) {
|
||||
// if there is no item id assume it's a new item
|
||||
this.genericAPI(this.this_model, this.Actions.CREATE, item)
|
||||
.then((result) => {
|
||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{ ...result.data }].concat(this.destroyCard(result?.data?.id, this.items_right))
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.this_model, this.Actions.UPDATE, item)
|
||||
.then((result) => {
|
||||
this.refreshThis(item.id)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
},
|
||||
// this currently assumes shopping is only applicable on FOOD model
|
||||
addShopping: function (food) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user