More localization (#358)

* Translate missing items on About page

* Localize import summary dialog

* Make site menu translation reactive

* Localize import options

* Include semi colon in string

* Move API texts to frontend + better status codes

* Provide feedback to user when no meal is planned

* Fix API tests after latest rework

* Add warning for API changes in changelog

* Refactor API texts handling

* Refactor API texts handling #2

* Better API feedback

* Rearrange strings hierarchy

* Add messages upon recipe updated

* Fix 'recipe effected' typo

* Remove snackbar usage in backend

* Translate toolbox

* Provide feedback for tags CRUD

* Fix messed up merge

* Translate sign-up form

* Better feedback for sign-up CRUD

* Refactor log-in API texts handling

* No error message when user is not authenticated

* Remove unimportant console log
This commit is contained in:
sephrat 2021-04-29 18:22:45 +02:00 committed by GitHub
parent 861020ffe0
commit 1e5edc7434
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 890 additions and 606 deletions

View File

@ -17,6 +17,8 @@
- Fixes #281 - Slow Handling of Large Sets of Recipes - Fixes #281 - Slow Handling of Large Sets of Recipes
## Features and Improvements ## Features and Improvements
- 'Dinner this week' shows a warning when no meal is planned yet
- 'Dinner today' shows a warning when no meal is planned yet
### General ### General
- New Toolbox Page! - New Toolbox Page!
@ -37,4 +39,7 @@
### Behind the Scenes ### Behind the Scenes
- Unified Sidebar Components - Unified Sidebar Components
- Refactor UI components to fit Vue best practices (WIP) - Refactor UI components to fit Vue best practices (WIP)
- The API returns more consistent status codes
- The API returns error code instead of error text when appropriate
- ⚠️ May cause side-effects if you were directly consuming the API

View File

@ -1,75 +1,57 @@
const baseURL = "/api/"; const baseURL = "/api/";
import axios from "axios"; import axios from "axios";
import utils from "@/utils";
import { store } from "../store"; import { store } from "../store";
import utils from "@/utils";
axios.defaults.headers.common[ axios.defaults.headers.common[
"Authorization" "Authorization"
] = `Bearer ${store.getters.getToken}`; ] = `Bearer ${store.getters.getToken}`;
function processResponse(response) { function handleError(error, getText) {
try { if(getText) {
utils.notify.show(response.data.snackbar.text, response.data.snackbar.type); utils.notify.error(getText(error.response));
} catch (err) {
return;
} }
return false;
}
function handleResponse(response, getText) {
if(response && getText) {
const successText = getText(response);
utils.notify.success(successText);
}
return response;
}
return; function defaultErrorText(response) {
return response.statusText;
}
function defaultSuccessText(response) {
return response.statusText;
} }
const apiReq = { const apiReq = {
post: async function(url, data) { post: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
let response = await axios.post(url, data).catch(function(error) { const response = await axios.post(url, data).catch(function(error) { handleError(error, getErrorText) });
if (error.response) { return handleResponse(response, getSuccessText);
processResponse(error.response); },
return error.response;
} put: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
}); const response = await axios.put(url, data).catch(function(error) { handleError(error, getErrorText) });
processResponse(response); return handleResponse(response, getSuccessText);
return response; },
patch: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
const response = await axios.patch(url, data).catch(function(error) { handleError(error, getErrorText) });
return handleResponse(response, getSuccessText);
}, },
put: async function(url, data) { get: function(url, data, getErrorText = defaultErrorText) {
let response = await axios.put(url, data).catch(function(error) { return axios.get(url, data).catch(function(error) { handleError(error, getErrorText) });
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
},
patch: async function(url, data) {
let response = await axios.patch(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
}, },
get: async function(url, data) { delete: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText ) {
let response = await axios.get(url, data).catch(function(error) { const response = await axios.delete(url, data).catch( function(error) { handleError(error, getErrorText) } );
if (error.response) { return handleResponse(response, getSuccessText);
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
},
delete: async function(url, data) {
let response = await axios.delete(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
}
});
processResponse(response);
return response;
}, },
async download(url) { async download(url) {

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "@/store"; import { store } from "@/store";
import i18n from '@/i18n.js';
const backupBase = baseURL + "backups/"; const backupBase = baseURL + "backups/";
@ -40,7 +41,12 @@ export const backupAPI = {
* @param {string} fileName * @param {string} fileName
*/ */
async delete(fileName) { async delete(fileName) {
await apiReq.delete(backupURLs.deleteBackup(fileName)); return apiReq.delete(
backupURLs.deleteBackup(fileName),
null,
function() { return i18n.t('settings.backup.unable-to-delete-backup'); },
function() { return i18n.t('settings.backup.backup-deleted'); }
);
}, },
/** /**
* Creates a backup on the serve given a set of options * Creates a backup on the serve given a set of options
@ -48,8 +54,12 @@ export const backupAPI = {
* @returns * @returns
*/ */
async create(options) { async create(options) {
let response = apiReq.post(backupURLs.createBackup, options); return apiReq.post(
return response; backupURLs.createBackup,
options,
function() { return i18n.t('settings.backup.error-creating-backup-see-log-file'); },
function(response) { return i18n.t('settings.backup.backup-created-at-response-export_path', {path: response.data.export_path}); }
);
}, },
/** /**
* Downloads a file from the server. I don't actually think this is used? * Downloads a file from the server. I don't actually think this is used?

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "@/store"; import { store } from "@/store";
import i18n from '@/i18n.js';
const prefix = baseURL + "categories"; const prefix = baseURL + "categories";
@ -22,29 +23,44 @@ export const categoryAPI = {
return response.data; return response.data;
}, },
async create(name) { async create(name) {
let response = await apiReq.post(categoryURLs.getAll, { name: name }); const response = await apiReq.post(
store.dispatch("requestCategories"); categoryURLs.getAll,
return response.data; { name: name },
function() { return i18n.t('category.category-creation-failed'); },
function() { return i18n.t('category.category-created'); }
);
if(response) {
store.dispatch("requestCategories");
return response.data;
}
}, },
async getRecipesInCategory(category) { async getRecipesInCategory(category) {
let response = await apiReq.get(categoryURLs.getCategory(category)); let response = await apiReq.get(categoryURLs.getCategory(category));
return response.data; return response.data;
}, },
async update(name, newName, overrideRequest = false) { async update(name, newName, overrideRequest = false) {
let response = await apiReq.put(categoryURLs.updateCategory(name), { const response = await apiReq.put(
name: newName, categoryURLs.updateCategory(name),
}); { name: newName },
if (!overrideRequest) { function() { return i18n.t('category.category-update-failed'); },
function() { return i18n.t('category.category-updated'); }
);
if (response && !overrideRequest) {
store.dispatch("requestCategories"); store.dispatch("requestCategories");
return response.data;
} }
return response.data;
}, },
async delete(category, overrideRequest = false) { async delete(category, overrideRequest = false) {
let response = await apiReq.delete(categoryURLs.deleteCategory(category)); const response = await apiReq.delete(
if (!overrideRequest) { categoryURLs.deleteCategory(category),
null,
function() { return i18n.t('category.category-deletion-failed'); },
function() { return i18n.t('category.category-deleted'); }
);
if (response && !overrideRequest) {
store.dispatch("requestCategories"); store.dispatch("requestCategories");
} }
return response.data; return response;
}, },
}; };
@ -68,28 +84,48 @@ export const tagAPI = {
return response.data; return response.data;
}, },
async create(name) { async create(name) {
let response = await apiReq.post(tagURLs.getAll, { name: name }); const response = await apiReq.post(
store.dispatch("requestTags"); tagURLs.getAll,
return response.data; { name: name },
function() { return i18n.t('tag.tag-creation-failed'); },
function() { return i18n.t('tag.tag-created'); }
);
if(response) {
store.dispatch("requestTags");
return response.data;
}
}, },
async getRecipesInTag(tag) { async getRecipesInTag(tag) {
let response = await apiReq.get(tagURLs.getTag(tag)); let response = await apiReq.get(tagURLs.getTag(tag));
return response.data; return response.data;
}, },
async update(name, newName, overrideRequest = false) { async update(name, newName, overrideRequest = false) {
let response = await apiReq.put(tagURLs.updateTag(name), { name: newName }); const response = await apiReq.put(
tagURLs.updateTag(name),
{ name: newName },
function() { return i18n.t('tag.tag-update-failed'); },
function() { return i18n.t('tag.tag-updated'); }
);
if (!overrideRequest) { if(response) {
store.dispatch("requestTags"); if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data;
} }
return response.data;
}, },
async delete(tag, overrideRequest = false) { async delete(tag, overrideRequest = false) {
let response = await apiReq.delete(tagURLs.deleteTag(tag)); const response = await apiReq.delete(
if (!overrideRequest) { tagURLs.deleteTag(tag),
store.dispatch("requestTags"); null,
function() { return i18n.t('tag.tag-deletion-failed'); },
function() { return i18n.t('tag.tag-deleted'); }
);
if(response) {
if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data;
} }
return response.data;
}, },
}; };

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const groupPrefix = baseURL + "groups"; const groupPrefix = baseURL + "groups";
const groupsURLs = { const groupsURLs = {
@ -10,25 +11,58 @@ const groupsURLs = {
update: id => `${groupPrefix}/${id}`, update: id => `${groupPrefix}/${id}`,
}; };
function deleteErrorText(response) {
switch(response.data.detail) {
case 'GROUP_WITH_USERS':
return i18n.t('group.cannot-delete-group-with-users');
case 'GROUP_NOT_FOUND':
return i18n.t('group.group-not-found');
case 'DEFAULT_GROUP':
return i18n.t('group.cannot-delete-default-group');
default:
return i18n.t('group.group-deletion-failed');
}
}
export const groupAPI = { export const groupAPI = {
async allGroups() { async allGroups() {
let response = await apiReq.get(groupsURLs.groups); let response = await apiReq.get(groupsURLs.groups);
return response.data; return response.data;
}, },
async create(name) { create(name) {
let response = await apiReq.post(groupsURLs.create, { name: name }); return apiReq.post(
return response.data; groupsURLs.create,
{ name: name },
function() { return i18n.t('group.user-group-creation-failed'); },
function() { return i18n.t('group.user-group-created'); }
);
}, },
async delete(id) { delete(id) {
let response = await apiReq.delete(groupsURLs.delete(id)); return apiReq.delete(
return response.data; groupsURLs.delete(id),
null,
deleteErrorText,
function() { return i18n.t('group.group-deleted'); }
);
}, },
async current() { async current() {
let response = await apiReq.get(groupsURLs.current); const response = await apiReq.get(
return response.data; groupsURLs.current,
null,
null);
if(response) {
return response.data;
}
}, },
async update(data) { update(data) {
let response = await apiReq.put(groupsURLs.update(data.id), data); return apiReq.put(
return response.data; groupsURLs.update(data.id),
data,
function() { return i18n.t('group.error-updating-group'); },
function() { return i18n.t('settings.group-settings-updated'); }
);
}, },
}; };

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const prefix = baseURL + "meal-plans/"; const prefix = baseURL + "meal-plans/";
@ -15,9 +16,13 @@ const mealPlanURLs = {
}; };
export const mealplanAPI = { export const mealplanAPI = {
async create(postBody) { create(postBody) {
let response = await apiReq.post(mealPlanURLs.create, postBody); return apiReq.post(
return response; mealPlanURLs.create,
postBody,
function() { return i18n.t('meal-plan.mealplan-creation-failed')},
function() { return i18n.t('meal-plan.mealplan-created'); }
);
}, },
async all() { async all() {
@ -35,14 +40,21 @@ export const mealplanAPI = {
return response; return response;
}, },
async delete(id) { delete(id) {
let response = await apiReq.delete(mealPlanURLs.delete(id)); return apiReq.delete(mealPlanURLs.delete(id),
return response; null,
function() { return i18n.t('meal-plan.mealplan-deletion-failed'); },
function() { return i18n.t('meal-plan.mealplan-deleted'); }
);
}, },
async update(id, body) { update(id, body) {
let response = await apiReq.put(mealPlanURLs.update(id), body); return apiReq.put(
return response; mealPlanURLs.update(id),
body,
function() { return i18n.t('meal-plan.mealplan-update-failed'); },
function() { return i18n.t('meal-plan.mealplan-updated'); }
);
}, },
async shoppingList(id) { async shoppingList(id) {

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "../store"; import { store } from "../store";
import i18n from '@/i18n.js';
const migrationBase = baseURL + "migrations"; const migrationBase = baseURL + "migrations";
@ -17,8 +18,13 @@ export const migrationAPI = {
return response.data; return response.data;
}, },
async delete(folder, file) { async delete(folder, file) {
let response = await apiReq.delete(migrationURLs.delete(folder, file)); const response = await apiReq.delete(
return response.data; migrationURLs.delete(folder, file),
null,
function() { return i18n.t('general.file-folder-not-found'); },
function() { return i18n.t('migration.migration-data-removed'); }
);
return response;
}, },
async import(folder, file) { async import(folder, file) {
let response = await apiReq.post(migrationURLs.import(folder, file)); let response = await apiReq.post(migrationURLs.import(folder, file));

View File

@ -1,7 +1,7 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "../store"; import { store } from "../store";
import { router } from "../main"; import i18n from '@/i18n.js';
const prefix = baseURL + "recipes/"; const prefix = baseURL + "recipes/";
@ -26,9 +26,12 @@ export const recipeAPI = {
* @returns {string} Recipe Slug * @returns {string} Recipe Slug
*/ */
async createByURL(recipeURL) { async createByURL(recipeURL) {
let response = await apiReq.post(recipeURLs.createByURL, { const response = await apiReq.post(
url: recipeURL, recipeURLs.createByURL,
}); { url: recipeURL },
function() { return i18n.t('recipe.recipe-creation-failed'); },
function() { return i18n.t('recipe.recipe-created'); }
);
store.dispatch("requestRecentRecipes"); store.dispatch("requestRecentRecipes");
return response; return response;
@ -43,7 +46,12 @@ export const recipeAPI = {
}, },
async create(recipeData) { async create(recipeData) {
let response = await apiReq.post(recipeURLs.create, recipeData); const response = await apiReq.post(
recipeURLs.create,
recipeData,
function() { return i18n.t('recipe.recipe-creation-failed'); },
function() { return i18n.t('recipe.recipe-created'); }
);
store.dispatch("requestRecentRecipes"); store.dispatch("requestRecentRecipes");
return response.data; return response.data;
}, },
@ -53,14 +61,24 @@ export const recipeAPI = {
return response.data; return response.data;
}, },
async updateImage(recipeSlug, fileObject) { updateImage(recipeSlug, fileObject, overrideSuccessMsg = false) {
const fd = new FormData(); const formData = new FormData();
fd.append("image", fileObject); formData.append("image", fileObject);
fd.append("extension", fileObject.name.split(".").pop()); formData.append("extension", fileObject.name.split(".").pop());
let response = apiReq.put(recipeURLs.updateImage(recipeSlug), fd);
return response;
},
let successMessage = null;
if(!overrideSuccessMsg) {
successMessage = function() { return overrideSuccessMsg ? null : i18n.t('recipe.recipe-image-updated'); };
}
return apiReq.put(
recipeURLs.updateImage(recipeSlug),
formData,
function() { return i18n.t('general.image-upload-failed'); },
successMessage
);
},
async createAsset(recipeSlug, fileObject, name, icon) { async createAsset(recipeSlug, fileObject, name, icon) {
const fd = new FormData(); const fd = new FormData();
fd.append("file", fileObject); fd.append("file", fileObject);
@ -70,17 +88,27 @@ export const recipeAPI = {
let response = apiReq.post(recipeURLs.createAsset(recipeSlug), fd); let response = apiReq.post(recipeURLs.createAsset(recipeSlug), fd);
return response; return response;
}, },
async updateImagebyURL(slug, url) { updateImagebyURL(slug, url) {
const response = apiReq.post(recipeURLs.updateImage(slug), { url: url }); return apiReq.post(
return response; recipeURLs.updateImage(slug),
{ url: url },
function() { return i18n.t('general.image-upload-failed'); },
function() { return i18n.t('recipe.recipe-image-updated'); }
);
}, },
async update(data) { async update(data) {
console.log(data) let response = await apiReq.put(
let response = await apiReq.put(recipeURLs.update(data.slug), data); recipeURLs.update(data.slug),
store.dispatch("patchRecipe", response.data); data,
return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request function() { return i18n.t('recipe.recipe-update-failed'); },
function() { return i18n.t('recipe.recipe-updated'); }
);
if(response) {
store.dispatch("patchRecipe", response.data);
return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request
}
}, },
async patch(data) { async patch(data) {
@ -89,10 +117,13 @@ export const recipeAPI = {
return response.data; return response.data;
}, },
async delete(recipeSlug) { delete(recipeSlug) {
await apiReq.delete(recipeURLs.delete(recipeSlug)); return apiReq.delete(
store.dispatch("requestRecentRecipes"); recipeURLs.delete(recipeSlug),
router.push(`/`); null,
function() { return i18n.t('recipe.unable-to-delete-recipe'); },
function() { return i18n.t('recipe.recipe-deleted'); }
);
}, },
async allSummary(start = 0, limit = 9999) { async allSummary(start = 0, limit = 9999) {

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const signUpPrefix = baseURL + "users/sign-ups"; const signUpPrefix = baseURL + "users/sign-ups";
@ -16,15 +17,25 @@ export const signupAPI = {
return response.data; return response.data;
}, },
async createToken(data) { async createToken(data) {
let response = await apiReq.post(signUpURLs.createToken, data); let response = await apiReq.post(
signUpURLs.createToken,
data,
function() { return i18n.t('signup.sign-up-link-creation-failed'); },
function() { return i18n.t('signup.sign-up-link-created'); }
);
return response.data; return response.data;
}, },
async deleteToken(token) { async deleteToken(token) {
let response = await apiReq.delete(signUpURLs.deleteToken(token)); return await apiReq.delete(signUpURLs.deleteToken(token),
return response.data; null,
function() { return i18n.t('signup.sign-up-token-deletion-failed'); },
function() { return i18n.t('signup.sign-up-token-deleted'); }
);
}, },
async createUser(token, data) { async createUser(token, data) {
let response = await apiReq.post(signUpURLs.createUser(token), data); return apiReq.post(signUpURLs.createUser(token), data,
return response.data; function() { return i18n.t('user.you-are-not-allowed-to-create-a-user'); },
function() { return i18n.t('user.user-created'); }
);
}, },
}; };

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "@/store"; import { store } from "@/store";
import i18n from '@/i18n.js';
const settingsBase = baseURL + "site-settings"; const settingsBase = baseURL + "site-settings";
@ -19,9 +20,16 @@ export const siteSettingsAPI = {
}, },
async update(body) { async update(body) {
let response = await apiReq.put(settingsURLs.updateSiteSettings, body); const response = await apiReq.put(
store.dispatch("requestSiteSettings"); settingsURLs.updateSiteSettings,
return response.data; body,
function() { return i18n.t('settings.settings-update-failed'); },
function() { return i18n.t('settings.settings-updated'); }
);
if(response) {
store.dispatch("requestSiteSettings");
}
return response;
}, },
async getPages() { async getPages() {
@ -34,23 +42,39 @@ export const siteSettingsAPI = {
return response.data; return response.data;
}, },
async createPage(body) { createPage(body) {
let response = await apiReq.post(settingsURLs.customPages, body); return apiReq.post(
return response.data; settingsURLs.customPages,
body,
function() { return i18n.t('page.page-creation-failed'); },
function() { return i18n.t('page.new-page-created'); }
);
}, },
async deletePage(id) { async deletePage(id) {
let response = await apiReq.delete(settingsURLs.customPage(id)); return await apiReq.delete(
return response.data; settingsURLs.customPage(id),
null,
function() { return i18n.t('page.page-deletion-failed'); },
function() { return i18n.t('page.page-deleted'); });
}, },
async updatePage(body) { updatePage(body) {
let response = await apiReq.put(settingsURLs.customPage(body.id), body); return apiReq.put(
return response.data; settingsURLs.customPage(body.id),
body,
function() { return i18n.t('page.page-update-failed'); },
function() { return i18n.t('page.page-updated'); }
);
}, },
async updateAllPages(allPages) { async updateAllPages(allPages) {
let response = await apiReq.put(settingsURLs.customPages, allPages); let response = await apiReq.put(
settingsURLs.customPages,
allPages,
function() { return i18n.t('page.pages-update-failed'); },
function() { return i18n.t('page.pages-updated'); }
);
return response; return response;
}, },
}; };

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const prefix = baseURL + "themes"; const prefix = baseURL + "themes";
@ -23,21 +24,31 @@ export const themeAPI = {
}, },
async create(postBody) { async create(postBody) {
let response = await apiReq.post(settingsURLs.createTheme, postBody); return await apiReq.post(
return response.data; settingsURLs.createTheme,
postBody,
function() { return i18n.t('settings.theme.error-creating-theme-see-log-file'); },
function() { return i18n.t('settings.theme.theme-saved'); });
}, },
async update(themeName, colors) { update(themeName, colors) {
const body = { const body = {
name: themeName, name: themeName,
colors: colors, colors: colors,
}; };
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body); return apiReq.put(
return response.data; settingsURLs.updateTheme(themeName),
body,
function() { return i18n.t('settings.theme.error-updating-theme'); },
function() { return i18n.t('settings.theme.theme-updated'); });
}, },
async delete(themeName) { delete(themeName) {
let response = await apiReq.delete(settingsURLs.deleteTheme(themeName)); return apiReq.delete(
return response.data; settingsURLs.deleteTheme(themeName),
null,
function() { return i18n.t('settings.theme.error-deleting-theme'); },
function() { return i18n.t('settings.theme.theme-deleted'); }
);
}, },
}; };

View File

@ -1,15 +1,16 @@
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
export const utilsAPI = { export const utilsAPI = {
// import { api } from "@/api"; // import { api } from "@/api";
async uploadFile(url, fileObject) { uploadFile(url, fileObject) {
console.log("API Called"); console.log("API Called");
let response = await apiReq.post(url, fileObject, { return apiReq.post(
headers: { url,
"Content-Type": "multipart/form-data", fileObject,
}, function() { return i18n.t('general.failure-uploading-file'); },
}); function() { return i18n.t('general.file-uploaded'); }
return response.data; );
}, },
}; };

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import axios from "axios"; import axios from "axios";
import i18n from '@/i18n.js';
const authPrefix = baseURL + "auth"; const authPrefix = baseURL + "auth";
const userPrefix = baseURL + "users"; const userPrefix = baseURL + "users";
@ -17,13 +18,23 @@ const usersURLs = {
resetPassword: id => `${userPrefix}/${id}/reset-password`, resetPassword: id => `${userPrefix}/${id}/reset-password`,
}; };
function deleteErrorText(response) {
switch(response.data.detail) {
case 'SUPER_USER':
return i18n.t('user.error-cannot-delete-super-user');
default:
return i18n.t('user.you-are-not-allowed-to-delete-this-user');
}
}
export const userAPI = { export const userAPI = {
async login(formData) { async login(formData) {
let response = await apiReq.post(authURLs.token, formData, { let response = await apiReq.post(
headers: { authURLs.token,
"Content-Type": "application/x-www-form-urlencoded", formData,
}, function() { return i18n.t('user.incorrect-username-or-password'); },
}); function() { return i18n.t('user.user-successfully-logged-in'); }
);
return response; return response;
}, },
async refresh() { async refresh() {
@ -36,9 +47,13 @@ export const userAPI = {
let response = await apiReq.get(usersURLs.users); let response = await apiReq.get(usersURLs.users);
return response.data; return response.data;
}, },
async create(user) { create(user) {
let response = await apiReq.post(usersURLs.users, user); return apiReq.post(
return response.data; usersURLs.users,
user,
function() { return i18n.t('user.user-creation-failed'); },
function() { return i18n.t('user.user-created'); }
);
}, },
async self() { async self() {
let response = await apiReq.get(usersURLs.self); let response = await apiReq.get(usersURLs.self);
@ -48,20 +63,37 @@ export const userAPI = {
let response = await apiReq.get(usersURLs.userID(id)); let response = await apiReq.get(usersURLs.userID(id));
return response.data; return response.data;
}, },
async update(user) { update(user) {
let response = await apiReq.put(usersURLs.userID(user.id), user); return apiReq.put(
return response.data; usersURLs.userID(user.id),
user,
function() { return i18n.t('user.user-update-failed'); },
function() { return i18n.t('user.user-updated'); }
);
}, },
async changePassword(id, password) { changePassword(id, password) {
let response = await apiReq.put(usersURLs.password(id), password); return apiReq.put(
return response.data; usersURLs.password(id),
password,
function() { return i18n.t('user.existing-password-does-not-match'); },
function() { return i18n.t('user.password-updated'); }
);
}, },
async delete(id) {
let response = await apiReq.delete(usersURLs.userID(id)); delete(id) {
return response.data; return apiReq.delete(
usersURLs.userID(id),
null,
deleteErrorText,
function() { return i18n.t('user.user-deleted'); }
);
}, },
async resetPassword(id) { resetPassword(id) {
let response = await apiReq.put(usersURLs.resetPassword(id)); return apiReq.put(
return response.data; usersURLs.resetPassword(id),
null,
function() { return i18n.t('user.password-reset-failed'); },
function() { return i18n.t('user.password-has-been-reset-to-the-default-password'); }
);
}, },
}; };

View File

@ -90,7 +90,7 @@ export default {
computed: { computed: {
inputLabel() { inputLabel() {
if (!this.showLabel) return null; if (!this.showLabel) return null;
return this.tagSelector ? this.$t('recipe.tags') : this.$t('recipe.categories'); return this.tagSelector ? this.$t('tag.tags') : this.$t('recipe.categories');
}, },
activeItems() { activeItems() {
let ItemObjects = []; let ItemObjects = [];

View File

@ -7,7 +7,7 @@
mdi-import mdi-import
</v-icon> </v-icon>
<v-toolbar-title class="headline"> <v-toolbar-title class="headline">
Import Summary {{ $t("settings.backup.import-summary") }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
</v-app-bar> </v-app-bar>
@ -18,8 +18,8 @@
<div> <div>
<h3>{{ values.title }}</h3> <h3>{{ values.title }}</h3>
</div> </div>
<div class="success--text">Success: {{ values.success }}</div> <div class="success--text">{{ $t("general.success-count", { count: values.success }) }}</div>
<div class="error--text">Failed: {{ values.failure }}</div> <div class="error--text">{{ $t("general.failed-count", { count: values.failure }) }}</div>
</v-card-text> </v-card-text>
</div> </div>
</v-row> </v-row>
@ -28,7 +28,7 @@
<v-tab>{{ $t("general.recipes") }}</v-tab> <v-tab>{{ $t("general.recipes") }}</v-tab>
<v-tab>{{ $t("general.themes") }}</v-tab> <v-tab>{{ $t("general.themes") }}</v-tab>
<v-tab>{{ $t("general.settings") }}</v-tab> <v-tab>{{ $t("general.settings") }}</v-tab>
<v-tab> Pages </v-tab> <v-tab> {{ $t("settings.pages") }} </v-tab>
<v-tab>{{ $t("general.users") }}</v-tab> <v-tab>{{ $t("general.users") }}</v-tab>
<v-tab>{{ $t("general.groups") }}</v-tab> <v-tab>{{ $t("general.groups") }}</v-tab>
</v-tabs> </v-tabs>
@ -59,24 +59,30 @@ export default {
userData: [], userData: [],
groupData: [], groupData: [],
pageData: [], pageData: [],
importHeaders: [
{
text: "Status",
value: "status",
},
{
text: "Name",
align: "start",
sortable: true,
value: "name",
},
{ text: "Exception", value: "data-table-expand", align: "center" },
],
allDataTables: [], allDataTables: [],
}), }),
computed: { computed: {
importHeaders() {
return [
{
text: this.$t('general.status'),
value: "status",
},
{
text: this.$t('general.name'),
align: "start",
sortable: true,
value: "name",
},
{
text: this.$t('general.exception'),
value: "data-table-expand",
align: "center"
},
]
},
recipeNumbers() { recipeNumbers() {
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData); return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
}, },
@ -96,7 +102,7 @@ export default {
return this.calculateNumbers(this.$t("general.groups"), this.groupData); return this.calculateNumbers(this.$t("general.groups"), this.groupData);
}, },
pageNumbers() { pageNumbers() {
return this.calculateNumbers("Pages", this.pageData); return this.calculateNumbers(this.$t("settings.pages"), this.pageData);
}, },
allNumbers() { allNumbers() {
return [ return [

View File

@ -91,18 +91,12 @@ export default {
let formData = new FormData(); let formData = new FormData();
formData.append("username", this.user.email); formData.append("username", this.user.email);
formData.append("password", this.user.password); formData.append("password", this.user.password);
let key; const response = await api.users.login(formData);
try { if (!response) {
key = await api.users.login(formData);
} catch {
this.error = true; this.error = true;
}
if (key.status != 200) {
this.error = true;
this.loading = false;
} else { } else {
this.clear(); this.clear();
this.$store.commit("setToken", key.data.access_token); this.$store.commit("setToken", response.data.access_token);
this.$emit("logged-in"); this.$emit("logged-in");
} }

View File

@ -13,13 +13,13 @@
class="mr-2" class="mr-2"
> >
</v-progress-circular> </v-progress-circular>
<v-toolbar-title class="headline"> Sign Up </v-toolbar-title> <v-toolbar-title class="headline">
{{$t('signup.sign-up')}}
</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
</v-app-bar> </v-app-bar>
<v-card-text> <v-card-text>
Welcome to Mealie! To become a user of this instance you are required to {{$t('signup.welcome-to-mealie')}}
have a valid invitation link. If you haven't recieved an invitation you
are unable to sign-up. To recieve a link, contact the sites administrator.
<v-divider class="mt-3"></v-divider> <v-divider class="mt-3"></v-divider>
<v-form ref="signUpForm" @submit.prevent="signUp"> <v-form ref="signUpForm" @submit.prevent="signUp">
<v-text-field <v-text-field
@ -28,7 +28,7 @@
prepend-icon="mdi-account" prepend-icon="mdi-account"
validate-on-blur validate-on-blur
:rules="[existsRule]" :rules="[existsRule]"
label="Display Name" :label="$t('signup.display-name')"
type="email" type="email"
></v-text-field> ></v-text-field>
<v-text-field <v-text-field
@ -59,7 +59,7 @@
:type="showPassword ? 'text' : 'password'" :type="showPassword ? 'text' : 'password'"
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'" :append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
:rules="[ :rules="[
user.password === user.passwordConfirm || 'Password must match', user.password === user.passwordConfirm || $t('user.password-must-match'),
]" ]"
@click:append="showPassword = !showPassword" @click:append="showPassword = !showPassword"
></v-text-field> ></v-text-field>
@ -71,11 +71,11 @@
block="block" block="block"
type="submit" type="submit"
> >
Sign Up {{$t('signup.sign-up')}}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
<v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error"> <v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error">
Error Signing Up {{$t('signup.error-signing-up')}}
</v-alert> </v-alert>
</v-form> </v-form>
</v-card-text> </v-card-text>
@ -132,18 +132,16 @@ export default {
admin: false, admin: false,
}; };
let successUser = false;
if (this.$refs.signUpForm.validate()) { if (this.$refs.signUpForm.validate()) {
let response = await api.signUps.createUser(this.token, userData); if (await api.signUps.createUser(this.token, userData)) {
successUser = response.snackbar.text.includes("Created"); this.$emit("user-created");
this.$router.push("/");
}
} }
this.$emit("user-created");
this.loading = false; this.loading = false;
if (successUser) {
this.$router.push("/");
}
}, },
}, },
}; };

View File

@ -36,8 +36,9 @@ export default {
return utils.getDateAsPythonDate(dateObject); return utils.getDateAsPythonDate(dateObject);
}, },
async update() { async update() {
await api.mealPlans.update(this.mealPlan.uid, this.mealPlan); if (await api.mealPlans.update(this.mealPlan.uid, this.mealPlan)) {
this.$emit("updated"); this.$emit("updated");
}
}, },
}, },
}; };

View File

@ -197,11 +197,12 @@ export default {
endDate: this.endDate, endDate: this.endDate,
meals: this.meals, meals: this.meals,
}; };
await api.mealPlans.create(mealBody); if (await api.mealPlans.create(mealBody)) {
this.$emit(CREATE_EVENT); this.$emit(CREATE_EVENT);
this.meals = []; this.meals = [];
this.startDate = null; this.startDate = null;
this.endDate = null; this.endDate = null;
}
}, },
getImage(image) { getImage(image) {

View File

@ -6,7 +6,7 @@
<v-icon left> <v-icon left>
mdi-image mdi-image
</v-icon> </v-icon>
{{ $t("recipe.image") }} {{ $t("general.image") }}
</v-btn> </v-btn>
</template> </template>
<v-card width="400"> <v-card width="400">
@ -71,8 +71,9 @@ export default {
}, },
async getImageFromURL() { async getImageFromURL() {
this.loading = true; this.loading = true;
const response = await api.recipes.updateImagebyURL(this.slug, this.url); if (await api.recipes.updateImagebyURL(this.slug, this.url)) {
if (response) this.$emit(REFRESH_EVENT); this.$emit(REFRESH_EVENT);
}
this.loading = false; this.loading = false;
}, },
}, },

View File

@ -74,7 +74,7 @@
:show-label="false" :show-label="false"
/> />
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2> <h2 class="mt-4">{{ $t("tag.tags") }}</h2>
<CategoryTagSelector <CategoryTagSelector
:return-object="false" :return-object="false"
v-model="value.tags" v-model="value.tags"

View File

@ -44,7 +44,7 @@
</v-card> </v-card>
<v-card class="mt-2" v-if="tags.length > 0"> <v-card class="mt-2" v-if="tags.length > 0">
<v-card-title class="py-2"> <v-card-title class="py-2">
{{ $t("recipe.tags") }} {{ $t("tag.tags") }}
</v-card-title> </v-card-title>
<v-divider class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-text> <v-card-text>
@ -69,7 +69,7 @@
</v-row> </v-row>
<div v-if="!medium"> <div v-if="!medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" /> <RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" /> <RecipeChips :title="$t('tag.tags')" :items="tags" />
<Nutrition :value="nutrition" :edit="false" /> <Nutrition :value="nutrition" :edit="false" />
<Assets :value="assets" :edit="false" :slug="slug" /> <Assets :value="assets" :edit="false" :slug="slug" />
</div> </div>

View File

@ -55,10 +55,10 @@ export default {
let formData = new FormData(); let formData = new FormData();
formData.append(this.fileName, this.file); formData.append(this.fileName, this.file);
await api.utils.uploadFile(this.url, formData); if(await api.utils.uploadFile(this.url, formData)) {
this.$emit(UPLOAD_EVENT);
}
this.isSelecting = false; this.isSelecting = false;
this.$emit(UPLOAD_EVENT);
} }
}, },
onButtonClick() { onButtonClick() {

View File

@ -105,17 +105,15 @@ export default {
async createRecipe() { async createRecipe() {
if (this.$refs.urlForm.validate()) { if (this.$refs.urlForm.validate()) {
this.processing = true; this.processing = true;
let response = await api.recipes.createByURL(this.recipeURL); const response = await api.recipes.createByURL(this.recipeURL);
if (response.status !== 201) {
this.error = true;
this.processing = false;
return;
}
this.addRecipe = false;
this.processing = false; this.processing = false;
this.recipeURL = ""; if (response) {
this.$router.push(`/recipe/${response.data}`); this.addRecipe = false;
this.recipeURL = "";
this.$router.push(`/recipe/${response.data}`);
} else {
this.error = true;
}
} }
}, },

View File

@ -44,9 +44,9 @@ export default {
components: { components: {
LoginDialog, LoginDialog,
}, },
data: function() { computed: {
return { items() {
items: [ return [
{ {
icon: "mdi-account", icon: "mdi-account",
title: "Login", title: "Login",
@ -83,10 +83,8 @@ export default {
nav: "/admin", nav: "/admin",
restricted: true, restricted: true,
}, },
], ]
}; },
},
computed: {
filteredItems() { filteredItems() {
if (this.loggedIn) { if (this.loggedIn) {
return this.items.filter(x => x.restricted == true); return this.items.filter(x => x.restricted == true);

View File

@ -13,11 +13,22 @@
"demo": "Demo", "demo": "Demo",
"demo-status": "Demo Status", "demo-status": "Demo Status",
"development": "Development", "development": "Development",
"download-log": "Download Log",
"download-recipe-json": "Download Recipe JSON",
"not-demo": "Not Demo", "not-demo": "Not Demo",
"production": "Production", "production": "Production",
"sqlite-file": "SQLite File", "sqlite-file": "SQLite File",
"version": "Version" "version": "Version"
}, },
"category": {
"category-created": "Category created",
"category-creation-failed": "Category creation failed",
"category-deleted": "Category Deleted",
"category-deletion-failed": "Category deletion failed",
"category-filter": "Category Filter",
"category-update-failed": "Category update failed",
"category-updated": "Category updated"
},
"general": { "general": {
"apply": "Apply", "apply": "Apply",
"cancel": "Cancel", "cancel": "Cancel",
@ -30,29 +41,40 @@
"download": "Download", "download": "Download",
"edit": "Edit", "edit": "Edit",
"enabled": "Enabled", "enabled": "Enabled",
"exception": "Exception",
"failed-count": "Failed: {count}",
"failure-uploading-file": "Failure uploading file",
"field-required": "Field Required", "field-required": "Field Required",
"file-folder-not-found": "File/folder not found",
"file-uploaded": "File uploaded",
"filter": "Filter", "filter": "Filter",
"friday": "Friday", "friday": "Friday",
"get": "Get", "get": "Get",
"groups": "Groups", "groups": "Groups",
"image": "Image",
"image-upload-failed": "Image upload failed",
"import": "Import", "import": "Import",
"keyword": "Keyword",
"monday": "Monday", "monday": "Monday",
"name": "Name", "name": "Name",
"no": "No", "no": "No",
"ok": "OK", "ok": "OK",
"options": "Options", "options": "Options:",
"random": "Random", "random": "Random",
"recent": "Recent", "recent": "Recent",
"recipes": "Recipes", "recipes": "Recipes",
"rename-object": "Rename {0}",
"reset": "Reset", "reset": "Reset",
"saturday": "Saturday", "saturday": "Saturday",
"save": "Save", "save": "Save",
"settings": "Settings", "settings": "Settings",
"sort": "Sort", "sort": "Sort",
"sort-alphabetically": "A-Z", "sort-alphabetically": "A-Z",
"status": "Status",
"submit": "Submit", "submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday", "sunday": "Sunday",
"templates": "Templates", "templates": "Templates:",
"themes": "Themes", "themes": "Themes",
"thursday": "Thursday", "thursday": "Thursday",
"token": "Token", "token": "Token",
@ -64,6 +86,25 @@
"wednesday": "Wednesday", "wednesday": "Wednesday",
"yes": "Yes" "yes": "Yes"
}, },
"group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
"cannot-delete-default-group": "Cannot delete default group",
"cannot-delete-group-with-users": "Cannot delete group with users",
"confirm-group-deletion": "Confirm Group Deletion",
"create-group": "Create Group",
"error-updating-group": "Error updating group",
"group": "Group",
"group-deleted": "Group deleted",
"group-deletion-failed": "Group deletion failed",
"group-id-with-value": "Group ID: {groupID}",
"group-name": "Group Name",
"group-not-found": "Group not found",
"groups": "Groups",
"groups-can-only-be-set-by-administrators": "Groups can only be set by administrators",
"user-group": "User Group",
"user-group-created": "User Group Created",
"user-group-creation-failed": "User Group Creation Failed"
},
"meal-plan": { "meal-plan": {
"create-a-new-meal-plan": "Create a New Meal Plan", "create-a-new-meal-plan": "Create a New Meal Plan",
"dinner-this-week": "Dinner This Week", "dinner-this-week": "Dinner This Week",
@ -73,6 +114,14 @@
"group": "Group (Beta)", "group": "Group (Beta)",
"meal-planner": "Meal Planner", "meal-planner": "Meal Planner",
"meal-plans": "Meal Plans", "meal-plans": "Meal Plans",
"mealplan-created": "Mealplan created",
"mealplan-creation-failed": "Mealplan creation failed",
"mealplan-deleted": "Mealplan Deleted",
"mealplan-deletion-failed": "Mealplan deletion failed",
"mealplan-update-failed": "Mealplan update failed",
"mealplan-updated": "Mealplan Updated",
"no-meal-plan-defined-yet": "No meal plan defined yet",
"no-meal-planned-for-today": "No meal planned for today",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Only recipes with these categories will be used in Meal Plans", "only-recipes-with-these-categories-will-be-used-in-meal-plans": "Only recipes with these categories will be used in Meal Plans",
"planner": "Planner", "planner": "Planner",
"quick-week": "Quick Week", "quick-week": "Quick Week",
@ -84,6 +133,7 @@
"description": "Migrate data from Chowdown", "description": "Migrate data from Chowdown",
"title": "Chowdown" "title": "Chowdown"
}, },
"migration-data-removed": "Migration data removed",
"nextcloud": { "nextcloud": {
"description": "Migrate data from a Nextcloud Cookbook intance", "description": "Migrate data from a Nextcloud Cookbook intance",
"title": "Nextcloud Cookbook" "title": "Nextcloud Cookbook"
@ -102,23 +152,30 @@
"page": { "page": {
"all-recipes": "All Recipes", "all-recipes": "All Recipes",
"home-page": "Home Page", "home-page": "Home Page",
"new-page-created": "New page created",
"page-creation-failed": "Page creation failed",
"page-deleted": "Page deleted",
"page-deletion-failed": "Page deletion failed",
"page-update-failed": "Page update failed",
"page-updated": "Page updated",
"pages-update-failed": "Pages update failed",
"pages-updated": "Pages updated",
"recent": "Recent" "recent": "Recent"
}, },
"recipe": { "recipe": {
"assets": "Assets",
"add-key": "Add Key", "add-key": "Add Key",
"api-extras": "API Extras", "api-extras": "API Extras",
"assets": "Assets",
"calories": "Calories", "calories": "Calories",
"calories-suffix": "calories", "calories-suffix": "calories",
"carbohydrate-content": "Carbohydrate",
"categories": "Categories", "categories": "Categories",
"delete-confirmation": "Are you sure you want to delete this recipe?", "delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe", "delete-recipe": "Delete Recipe",
"description": "Description", "description": "Description",
"fat-content": "Fat", "fat-content": "Fat",
"fiber-content": "Fiber", "fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams", "grams": "grams",
"image": "Image",
"ingredient": "Ingredient", "ingredient": "Ingredient",
"ingredients": "Ingredients", "ingredients": "Ingredients",
"instructions": "Instructions", "instructions": "Instructions",
@ -135,27 +192,32 @@
"perform-time": "Cook Time", "perform-time": "Cook Time",
"prep-time": "Prep Time", "prep-time": "Prep Time",
"protein-content": "Protein", "protein-content": "Protein",
"recipe-created": "Recipe created",
"recipe-creation-failed": "Recipe creation failed",
"recipe-deleted": "Recipe deleted",
"recipe-image": "Recipe Image", "recipe-image": "Recipe Image",
"recipe-image-updated": "Recipe image updated",
"recipe-name": "Recipe Name", "recipe-name": "Recipe Name",
"recipe-update-failed": "Recipe update failed",
"recipe-updated": "Recipe updated",
"servings": "Servings", "servings": "Servings",
"sodium-content": "Sodium", "sodium-content": "Sodium",
"step-index": "Step: {step}", "step-index": "Step: {step}",
"sugar-content": "Sugar", "sugar-content": "Sugar",
"tags": "Tags",
"title": "Title", "title": "Title",
"total-time": "Total Time", "total-time": "Total Time",
"unable-to-delete-recipe": "Unable to Delete Recipe",
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"search-mealie": "Search Mealie (press /)", "and": "and",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -163,10 +225,15 @@
"admin-settings": "Admin Settings", "admin-settings": "Admin Settings",
"available-backups": "Available Backups", "available-backups": "Available Backups",
"backup": { "backup": {
"backup-created-at-response-export_path": "Backup Created at {path}",
"backup-deleted": "Backup deleted",
"backup-tag": "Backup Tag", "backup-tag": "Backup Tag",
"create-heading": "Create a Backup", "create-heading": "Create a Backup",
"error-creating-backup-see-log-file": "Error Creating Backup. See Log File",
"full-backup": "Full Backup", "full-backup": "Full Backup",
"partial-backup": "Partial Backup" "import-summary": "Import Summary",
"partial-backup": "Partial Backup",
"unable-to-delete-backup": "Unable to Delete Backup."
}, },
"backup-and-exports": "Backups", "backup-and-exports": "Backups",
"backup-info": "Backups are exported in standard JSON format along with all the images stored on the file system. In your backup folder you'll find a .zip file that contains all of the recipe JSON and images from the database. Additionally, if you selected a markdown file, those will also be stored in the .zip file. To import a backup, it must be located in your backups folder. Automated backups are done each day at 3:00 AM.", "backup-info": "Backups are exported in standard JSON format along with all the images stored on the file system. In your backup folder you'll find a .zip file that contains all of the recipe JSON and images from the database. Additionally, if you selected a markdown file, those will also be stored in the .zip file. To import a backup, it must be located in your backups folder. Automated backups are done each day at 3:00 AM.",
@ -175,6 +242,7 @@
"custom-pages": "Custom Pages", "custom-pages": "Custom Pages",
"edit-page": "Edit Page", "edit-page": "Edit Page",
"first-day-of-week": "First day of the week", "first-day-of-week": "First day of the week",
"group-settings-updated": "Group Settings Updated",
"homepage": { "homepage": {
"all-categories": "All Categories", "all-categories": "All Categories",
"card-per-section": "Card Per Section", "card-per-section": "Card Per Section",
@ -190,9 +258,12 @@
"migrations": "Migrations", "migrations": "Migrations",
"new-page": "New Page", "new-page": "New Page",
"page-name": "Page Name", "page-name": "Page Name",
"pages": "Pages",
"profile": "Profile", "profile": "Profile",
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries", "remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
"set-new-time": "Set New Time", "set-new-time": "Set New Time",
"settings-update-failed": "Settings update failed",
"settings-updated": "Settings updated",
"site-settings": "Site Settings", "site-settings": "Site Settings",
"theme": { "theme": {
"accent": "Accent", "accent": "Accent",
@ -203,6 +274,9 @@
"default-to-system": "Default to system", "default-to-system": "Default to system",
"delete-theme": "Delete Theme", "delete-theme": "Delete Theme",
"error": "Error", "error": "Error",
"error-creating-theme-see-log-file": "Error creating theme. See log file.",
"error-deleting-theme": "Error deleting theme",
"error-updating-theme": "Error updating theme",
"info": "Info", "info": "Info",
"light": "Light", "light": "Light",
"primary": "Primary", "primary": "Primary",
@ -210,51 +284,69 @@
"select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference": "Select a theme from the dropdown or create a new theme. Note that the default theme will be served to all users who have not set a theme preference.", "select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference": "Select a theme from the dropdown or create a new theme. Note that the default theme will be served to all users who have not set a theme preference.",
"success": "Success", "success": "Success",
"theme": "Theme", "theme": "Theme",
"theme-deleted": "Theme deleted",
"theme-name": "Theme Name", "theme-name": "Theme Name",
"theme-name-is-required": "Theme Name is required.", "theme-name-is-required": "Theme Name is required.",
"theme-saved": "Theme Saved",
"theme-settings": "Theme Settings", "theme-settings": "Theme Settings",
"theme-updated": "Theme updated",
"warning": "Warning" "warning": "Warning"
}, },
"toolbox": {
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign",
"new-name": "New Name",
"no-unused-items": "No Unused Items",
"recipes-affected": "No Recipes Affected|One Recipe Affected|{count} Recipes Affected",
"remove-unused": "Remove Unused",
"title-case-all": "Title Case All",
"toolbox": "Toolbox"
},
"webhooks": { "webhooks": {
"meal-planner-webhooks": "Meal Planner Webhooks", "meal-planner-webhooks": "Meal Planner Webhooks",
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"signup": {
"display-name": "Display Name",
"error-signing-up": "Error Signing Up",
"sign-up": "Sign Up",
"sign-up-link-created": "Sign up link created",
"sign-up-link-creation-failed": "Sign up link creation failed",
"sign-up-links": "Sign Up Links",
"sign-up-token-deleted": "Sign Up Token Deleted",
"sign-up-token-deletion-failed": "Sign up token deletion failed",
"welcome-to-mealie": "Welcome to Mealie! To become a user of this instance you are required to have a valid invitation link. If you haven't recieved an invitation you are unable to sign-up. To recieve a link, contact the sites administrator."
},
"tag": {
"tag-created": "Tag created",
"tag-creation-failed": "Tag creation failed",
"tag-deleted": "Tag deleted",
"tag-deletion-failed": "Tag deletion failed",
"tag-update-failed": "Tag update failed",
"tag-updated": "Tag updated",
"tags": "Tags"
},
"user": { "user": {
"admin": "Admin", "admin": "Admin",
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
"are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link <b>{link}<b/>?", "are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link <b>{link}<b/>?",
"are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?", "are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?",
"confirm-group-deletion": "Confirm Group Deletion",
"confirm-link-deletion": "Confirm Link Deletion", "confirm-link-deletion": "Confirm Link Deletion",
"confirm-password": "Confirm Password", "confirm-password": "Confirm Password",
"confirm-user-deletion": "Confirm User Deletion", "confirm-user-deletion": "Confirm User Deletion",
"could-not-validate-credentials": "Could Not Validate Credentials", "could-not-validate-credentials": "Could Not Validate Credentials",
"create-group": "Create Group",
"create-link": "Create Link", "create-link": "Create Link",
"create-user": "Create User", "create-user": "Create User",
"current-password": "Current Password", "current-password": "Current Password",
"e-mail-must-be-valid": "E-mail must be valid", "e-mail-must-be-valid": "E-mail must be valid",
"edit-user": "Edit User", "edit-user": "Edit User",
"email": "Email", "email": "Email",
"error-cannot-delete-super-user": "Error! Cannot Delete Super User",
"existing-password-does-not-match": "Existing password does not match",
"full-name": "Full Name", "full-name": "Full Name",
"group": "Group", "incorrect-username-or-password": "Incorrect username or password",
"group-id-with-value": "Group ID: {groupID}",
"group-name": "Group Name",
"groups": "Groups",
"groups-can-only-be-set-by-administrators": "Groups can only be set by administrators",
"link-id": "Link ID", "link-id": "Link ID",
"link-name": "Link Name", "link-name": "Link Name",
"login": "Login", "login": "Login",
@ -262,20 +354,29 @@
"new-password": "New Password", "new-password": "New Password",
"new-user": "New User", "new-user": "New User",
"password": "Password", "password": "Password",
"password-has-been-reset-to-the-default-password": "Password has been reset to the default password",
"password-must-match": "Password must match", "password-must-match": "Password must match",
"password-reset-failed": "Password reset failed",
"password-updated": "Password updated",
"reset-password": "Reset Password", "reset-password": "Reset Password",
"sign-in": "Sign in", "sign-in": "Sign in",
"sign-up-links": "Sign Up Links",
"total-mealplans": "Total MealPlans", "total-mealplans": "Total MealPlans",
"total-users": "Total Users", "total-users": "Total Users",
"upload-photo": "Upload Photo", "upload-photo": "Upload Photo",
"use-8-characters-or-more-for-your-password": "Use 8 characters or more for your password", "use-8-characters-or-more-for-your-password": "Use 8 characters or more for your password",
"user-group": "User Group", "user-created": "User created",
"user-creation-failed": "User creation failed",
"user-deleted": "User deleted",
"user-id": "User ID", "user-id": "User ID",
"user-id-with-value": "User ID: {id}", "user-id-with-value": "User ID: {id}",
"user-password": "User Password", "user-password": "User Password",
"user-successfully-logged-in": "User Successfully Logged In",
"user-update-failed": "User update failed",
"user-updated": "User updated",
"users": "Users", "users": "Users",
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled",
"you-are-not-allowed-to-create-a-user": "You are not allowed to create a user",
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user"
} }
} }

View File

@ -23,11 +23,11 @@
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<TheDownloadBtn <TheDownloadBtn
button-text="Download Recipe JSON" :button-text="$t('about.download-recipe-json')"
download-url="/api/debug/last-recipe-json" download-url="/api/debug/last-recipe-json"
/> />
<TheDownloadBtn <TheDownloadBtn
button-text="Download Log" :button-text="$t('about.download-log')"
download-url="/api/debug/log" download-url="/api/debug/log"
/> />
</v-card-actions> </v-card-actions>

View File

@ -62,17 +62,21 @@ export default {
}, },
async importBackup(data) { async importBackup(data) {
this.$emit("loading"); this.$emit("loading");
let response = await api.backups.import(data.name, data); const response = await api.backups.import(data.name, data);
if(response) {
let importData = response.data;
this.$emit("finished", importData);
} else {
this.$emit("finished");
}
let importData = response.data;
this.$emit("finished", importData);
}, },
deleteBackup(data) { async deleteBackup(data) {
this.$emit("loading"); this.$emit("loading");
api.backups.delete(data.name); if (await api.backups.delete(data.name)) {
this.selectedBackup = null; this.selectedBackup = null;
}
this.backupLoading = false; this.backupLoading = false;
this.$emit("finished"); this.$emit("finished");

View File

@ -28,7 +28,7 @@ export default {
}, },
pages: { pages: {
value: true, value: true,
text: "Pages", text: this.$t("settings.pages"),
}, },
themes: { themes: {
value: true, value: true,

View File

@ -20,11 +20,11 @@
<v-card-text class="mt-n4"> <v-card-text class="mt-n4">
<v-row> <v-row>
<v-col sm="4"> <v-col sm="4">
<p>{{ $t("general.options") }}:</p> <p>{{ $t("general.options") }}</p>
<ImportOptions @update-options="updateOptions" class="mt-5" /> <ImportOptions @update-options="updateOptions" class="mt-5" />
</v-col> </v-col>
<v-col> <v-col>
<p>{{ $t("general.templates") }}:</p> <p>{{ $t("general.templates") }}</p>
<v-checkbox <v-checkbox
v-for="template in availableTemplates" v-for="template in availableTemplates"
:key="template" :key="template"
@ -97,10 +97,11 @@ export default {
templates: this.selectedTemplates, templates: this.selectedTemplates,
}; };
await api.backups.create(data); if (await api.backups.create(data)) {
this.$emit("created");
}
this.loading = false; this.loading = false;
this.$emit("created");
}, },
appendTemplate(templateName) { appendTemplate(templateName) {
if (this.selectedTemplates.includes(templateName)) { if (this.selectedTemplates.includes(templateName)) {

View File

@ -2,9 +2,9 @@
<div> <div>
<ConfirmationDialog <ConfirmationDialog
ref="deleteGroupConfirm" ref="deleteGroupConfirm"
:title="$t('user.confirm-group-deletion')" :title="$t('group.confirm-group-deletion')"
:message=" :message="
$t('user.are-you-sure-you-want-to-delete-the-group', { $t('group.are-you-sure-you-want-to-delete-the-group', {
groupName: group.name, groupName: group.name,
}) })
" "
@ -18,7 +18,7 @@
<v-card-title class="py-1">{{ group.name }}</v-card-title> <v-card-title class="py-1">{{ group.name }}</v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-subheader>{{ <v-subheader>{{
$t("user.group-id-with-value", { groupID: group.id }) $t("group.group-id-with-value", { groupID: group.id })
}}</v-subheader> }}</v-subheader>
<v-list-item-group color="primary"> <v-list-item-group color="primary">
<v-list-item v-for="property in groupProps" :key="property.text"> <v-list-item v-for="property in groupProps" :key="property.text">
@ -91,8 +91,9 @@ export default {
this.$refs.deleteGroupConfirm.open(); this.$refs.deleteGroupConfirm.open();
}, },
async deleteGroup() { async deleteGroup() {
await api.groups.delete(this.group.id); if (await api.groups.delete(this.group.id)) {
this.$emit(RENDER_EVENT); this.$emit(RENDER_EVENT);
}
}, },
closeGroupDelete() { closeGroupDelete() {
console.log("Close Delete"); console.log("Close Delete");

View File

@ -24,7 +24,7 @@
v-bind="attrs" v-bind="attrs"
v-on="on" v-on="on"
> >
{{ $t("user.create-group") }} {{ $t("group.create-group") }}
</v-btn> </v-btn>
</template> </template>
<v-card> <v-card>
@ -34,7 +34,7 @@
</v-icon> </v-icon>
<v-toolbar-title class="headline"> <v-toolbar-title class="headline">
{{ $t("user.create-group") }} {{ $t("group.create-group") }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -43,7 +43,7 @@
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="newGroupName" v-model="newGroupName"
:label="$t('user.group-name')" :label="$t('group.group-name')"
:rules="[existsRule]" :rules="[existsRule]"
></v-text-field> ></v-text-field>
</v-card-text> </v-card-text>
@ -104,12 +104,11 @@ export default {
methods: { methods: {
async createGroup() { async createGroup() {
this.groupLoading = true; this.groupLoading = true;
let response = await api.groups.create(this.newGroupName); if (await api.groups.create(this.newGroupName)) {
if (response.created) {
this.groupLoading = false;
this.groupDialog = false; this.groupDialog = false;
this.$store.dispatch("requestAllGroups"); this.$store.dispatch("requestAllGroups");
} }
this.groupLoading = false;
}, },
}, },
}; };

View File

@ -1,7 +1,7 @@
<template> <template>
<v-card outlined class="mt-n1"> <v-card outlined class="mt-n1">
<ConfirmationDialog <ConfirmationDialog
ref="deleteUserDialog" ref="deleteTokenDialog"
:title="$t('user.confirm-link-deletion')" :title="$t('user.confirm-link-deletion')"
:message=" :message="
$t('user.are-you-sure-you-want-to-delete-the-link', { $t('user.are-you-sure-you-want-to-delete-the-link', {
@ -9,7 +9,7 @@
}) })
" "
icon="mdi-alert" icon="mdi-alert"
@confirm="deleteUser" @confirm="deleteToken"
:width="450" :width="450"
@close="closeDelete" @close="closeDelete"
/> />
@ -18,7 +18,7 @@
mdi-link-variant mdi-link-variant
</v-icon> </v-icon>
<v-toolbar-title class="headine"> <v-toolbar-title class="headine">
{{ $t("user.sign-up-links") }} {{ $t("signup.sign-up-links") }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer> </v-spacer> <v-spacer> </v-spacer>
@ -181,9 +181,10 @@ export default {
this.links = await api.signUps.getAll(); this.links = await api.signUps.getAll();
}, },
async deleteUser() { async deleteToken() {
await api.signUps.deleteToken(this.activeId); if (await api.signUps.deleteToken(this.activeId)) {
this.initialize(); this.initialize();
}
}, },
editItem(item) { editItem(item) {
@ -197,7 +198,7 @@ export default {
this.activeName = item.name; this.activeName = item.name;
this.editedIndex = this.links.indexOf(item); this.editedIndex = this.links.indexOf(item);
this.editedItem = Object.assign({}, item); this.editedItem = Object.assign({}, item);
this.$refs.deleteUserDialog.open(); this.$refs.deleteTokenDialog.open();
}, },
deleteItemConfirm() { deleteItemConfirm() {

View File

@ -72,7 +72,7 @@
dense dense
v-model="editedItem.group" v-model="editedItem.group"
:items="existingGroups" :items="existingGroups"
:label="$t('user.user-group')" :label="$t('group.user-group')"
></v-select> ></v-select>
</v-col> </v-col>
<v-col cols="12" sm="12" md="6" v-if="showPassword"> <v-col cols="12" sm="12" md="6" v-if="showPassword">
@ -94,7 +94,7 @@
<v-card-actions> <v-card-actions>
<v-btn color="info" text @click="resetPassword"> <v-btn color="info" text @click="resetPassword">
Reset Password {{$t('user.reset-password')}}
</v-btn> </v-btn>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="grey" text @click="close"> <v-btn color="grey" text @click="close">
@ -165,7 +165,7 @@ export default {
}, },
{ text: this.$t("user.full-name"), value: "fullName" }, { text: this.$t("user.full-name"), value: "fullName" },
{ text: this.$t("user.email"), value: "email" }, { text: this.$t("user.email"), value: "email" },
{ text: this.$t("user.group"), value: "group" }, { text: this.$t("group.group"), value: "group" },
{ text: this.$t("user.admin"), value: "admin" }, { text: this.$t("user.admin"), value: "admin" },
{ text: "", value: "actions", sortable: false, align: "center" }, { text: "", value: "actions", sortable: false, align: "center" },
], ],
@ -223,8 +223,9 @@ export default {
}, },
async deleteUser() { async deleteUser() {
await api.users.delete(this.activeId); if (await api.users.delete(this.activeId)) {
this.initialize(); this.initialize();
}
}, },
editItem(item) { editItem(item) {
@ -264,17 +265,27 @@ export default {
async save() { async save() {
if (this.editedIndex > -1) { if (this.editedIndex > -1) {
await api.users.update(this.editedItem); this.updateUser();
this.close();
} else if (this.$refs.newUser.validate()) { } else if (this.$refs.newUser.validate()) {
await api.users.create(this.editedItem); this.createUser();
this.close();
} }
await this.initialize(); await this.initialize();
}, },
resetPassword() { resetPassword() {
api.users.resetPassword(this.editedItem.id); api.users.resetPassword(this.editedItem.id);
}, },
async createUser() {
if(await api.users.create(this.editedItem)) {
this.close();
}
},
async updateUser() {
if(await api.users.update(this.editedItem)) {
this.close();
}
}
}, },
}; };
</script> </script>

View File

@ -16,12 +16,12 @@
</v-tab> </v-tab>
<v-tab> <v-tab>
{{ $t("user.sign-up-links") }} {{ $t("signup.sign-up-links") }}
<v-icon>mdi-account-plus-outline</v-icon> <v-icon>mdi-account-plus-outline</v-icon>
</v-tab> </v-tab>
<v-tab> <v-tab>
{{ $t("user.groups") }} {{ $t("group.groups") }}
<v-icon>mdi-account-group</v-icon> <v-icon>mdi-account-group</v-icon>
</v-tab> </v-tab>
</v-tabs> </v-tabs>

View File

@ -135,9 +135,10 @@ export default {
this.groupSettings.webhookUrls.splice(index, 1); this.groupSettings.webhookUrls.splice(index, 1);
}, },
async saveGroupSettings() { async saveGroupSettings() {
await api.groups.update(this.groupSettings); if (await api.groups.update(this.groupSettings)) {
await this.$store.dispatch("requestCurrentGroup"); await this.$store.dispatch("requestCurrentGroup");
this.getSiteSettings(); this.getSiteSettings();
}
}, },
testWebhooks() { testWebhooks() {
api.settings.testWebhooks(); api.settings.testWebhooks();

View File

@ -86,9 +86,10 @@ export default {
}; };
}, },
methods: { methods: {
deleteMigration(file_name) { async deleteMigration(file_name) {
api.migrations.delete(this.folder, file_name); if (await api.migrations.delete(this.folder, file_name)) {
this.$emit("refresh"); this.$emit("refresh");
}
}, },
async importMigration(file_name) { async importMigration(file_name) {
this.loading = true; this.loading = true;

View File

@ -55,11 +55,11 @@
> >
</v-text-field> </v-text-field>
<v-text-field <v-text-field
:label="$t('user.group')" :label="$t('group.group')"
readonly readonly
v-model="user.group" v-model="user.group"
persistent-hint persistent-hint
:hint="$t('user.groups-can-only-be-set-by-administrators')" :hint="$t('group.groups-can-only-be-set-by-administrators')"
> >
</v-text-field> </v-text-field>
</v-form> </v-form>
@ -201,11 +201,13 @@ export default {
}, },
async updateUser() { async updateUser() {
this.loading = true; this.loading = true;
let newKey = await api.users.update(this.user); const response = await api.users.update(this.user);
this.$store.commit("setToken", newKey.access_token); if(response) {
this.refreshProfile(); this.$store.commit("setToken", response.data.access_token);
this.loading = false; this.refreshProfile();
this.$store.dispatch("requestUserData"); this.loading = false;
this.$store.dispatch("requestUserData");
}
}, },
async changePassword() { async changePassword() {
this.paswordLoading = true; this.paswordLoading = true;
@ -215,7 +217,9 @@ export default {
}; };
if (this.$refs.passChange.validate()) { if (this.$refs.passChange.validate()) {
await api.users.changePassword(this.user.id, data); if (await api.users.changePassword(this.user.id, data)) {
this.$emit("refresh");
}
} }
this.paswordLoading = false; this.paswordLoading = false;
}, },

View File

@ -82,14 +82,18 @@ export default {
this.$refs.categoryFormSelector.setInit(this.page.categories); this.$refs.categoryFormSelector.setInit(this.page.categories);
}, },
async submitForm() { async submitForm() {
let response;
if (this.create) { if (this.create) {
await api.siteSettings.createPage(this.page); response = await api.siteSettings.createPage(this.page);
} else { } else {
await api.siteSettings.updatePage(this.page); response = await api.siteSettings.updatePage(this.page);
}
if (response) {
this.pageDialog = false;
this.page.categories = [];
this.$emit(NEW_PAGE_EVENT);
} }
this.pageDialog = false;
this.page.categories = [];
this.$emit(NEW_PAGE_EVENT);
}, },
}, },
}; };

View File

@ -109,9 +109,10 @@ export default {
element.position = index; element.position = index;
}); });
await api.siteSettings.updateAllPages(this.customPages); if (await api.siteSettings.updateAllPages(this.customPages)) {
this.getPages();
}
this.getPages();
}, },
editPage(index) { editPage(index) {
this.editPageData.data = this.customPages[index]; this.editPageData.data = this.customPages[index];

View File

@ -214,7 +214,7 @@ export default {
this.settings.language = val; this.settings.language = val;
}, },
deleteCategoryfromDatabase(category) { deleteCategoryfromDatabase(category) {
api.categories.delete(category); api.categories.delete(category);
}, },
async getOptions() { async getOptions() {
this.settings = await api.siteSettings.get(); this.settings = await api.siteSettings.get();
@ -223,8 +223,9 @@ export default {
this.settings.categories.splice(index, 1); this.settings.categories.splice(index, 1);
}, },
async saveSettings() { async saveSettings() {
await api.siteSettings.update(this.settings); if (await api.siteSettings.update(this.settings)) {
this.getOptions(); this.getOptions();
}
}, },
}, },
}; };

View File

@ -70,11 +70,11 @@ export default {
}, },
async deleteSelectedTheme() { async deleteSelectedTheme() {
//Delete Theme from DB //Delete Theme from DB
await api.themes.delete(this.theme.name); if (await api.themes.delete(this.theme.name)) {
//Get the new list of available from DB
//Get the new list of available from DB this.availableThemes = await api.themes.requestAll();
this.availableThemes = await api.themes.requestAll(); this.$emit(DELETE_EVENT);
this.$emit(DELETE_EVENT); }
}, },
async saveThemes() { async saveThemes() {
this.$store.commit("setTheme", this.theme); this.$store.commit("setTheme", this.theme);

View File

@ -171,9 +171,11 @@ export default {
* Create the new Theme and select it. * Create the new Theme and select it.
*/ */
async appendTheme(NewThemeDialog) { async appendTheme(NewThemeDialog) {
await api.themes.create(NewThemeDialog); const response = await api.themes.create(NewThemeDialog);
this.availableThemes.push(NewThemeDialog); if (response) {
this.$store.commit("setTheme", NewThemeDialog); this.availableThemes.push(NewThemeDialog);
this.$store.commit("setTheme", NewThemeDialog);
}
}, },
setStoresDarkMode() { setStoresDarkMode() {
this.$store.commit("setDarkMode", this.selectedDarkMode); this.$store.commit("setDarkMode", this.selectedDarkMode);
@ -181,8 +183,8 @@ export default {
/** /**
* This will save the current colors and make the selected theme live. * This will save the current colors and make the selected theme live.
*/ */
async saveThemes() { saveThemes() {
await api.themes.update( api.themes.update(
this.selectedTheme.name, this.selectedTheme.name,
this.selectedTheme.colors this.selectedTheme.colors
); );

View File

@ -4,7 +4,7 @@
ref="assignDialog" ref="assignDialog"
title-icon="mdi-tag" title-icon="mdi-tag"
color="primary" color="primary"
title="Bulk Assign" :title="$t('settings.toolbox.bulk-assign')"
:loading="loading" :loading="loading"
modal-width="700" modal-width="700"
:top="true" :top="true"
@ -13,7 +13,7 @@
<v-text-field <v-text-field
v-model="search" v-model="search"
autocomplete="off" autocomplete="off"
label="Keyword" :label="$t('general.keyword')"
></v-text-field> ></v-text-field>
<CategoryTagSelector <CategoryTagSelector
:tag-selector="false" :tag-selector="false"
@ -44,7 +44,7 @@
<v-card-title class="headline"> </v-card-title> <v-card-title class="headline"> </v-card-title>
<CardSection <CardSection
class="px-2 pb-2" class="px-2 pb-2"
:title="`${results.length || 0} Recipes Effected`" :title="$tc('settings.toolbox.recipes-affected', results.length || 0)"
:mobile-cards="true" :mobile-cards="true"
:recipes="results" :recipes="results"
:single-column="true" :single-column="true"

View File

@ -7,7 +7,7 @@
:title=" :title="
$t('general.delete') + $t('general.delete') +
' ' + ' ' +
(isTags ? $t('recipe.tags') : $t('recipe.categories')) (isTags ? $t('tag.tags') : $t('recipe.categories'))
" "
:loading="loading" :loading="loading"
modal-width="400" modal-width="400"

View File

@ -18,8 +18,7 @@
</v-form> </v-form>
<template slot="below-actions"> <template slot="below-actions">
<v-card-title class="headline"> <v-card-title class="headline">
{{ renameTarget.recipes.length || 0 }} {{ $tc("settings.toolbox.recipes-affected", renameTarget.recipes.length || 0) }}
{{ $t("settings.toolbox.recipes-effected") }}
</v-card-title> </v-card-title>
<MobileRecipeCard <MobileRecipeCard
class="ml-2 mr-2 mt-2 mb-2" class="ml-2 mr-2 mt-2 mb-2"
@ -94,10 +93,10 @@
<v-card-title class="py-1">{{ item.name }}</v-card-title> <v-card-title class="py-1">{{ item.name }}</v-card-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn small text color="info" @click="openEditDialog(item)"> <v-btn small text color="info" @click="openEditDialog(item)">
Edit {{$t('general.edit')}}
</v-btn> </v-btn>
<v-btn small text color="error" @click="deleteItem(item.slug)" <v-btn small text color="error" @click="deleteItem(item.slug)">
>Delete {{$t('general.delete')}}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@ -177,7 +176,7 @@ export default {
} }
this.renameTarget = { this.renameTarget = {
title: `Rename ${item.name}`, title:this.$t('general.rename-object', [item.name]),
name: item.name, name: item.name,
slug: item.slug, slug: item.slug,
newName: "", newName: "",

View File

@ -16,7 +16,7 @@
</v-tab> </v-tab>
<v-tab> <v-tab>
{{ $t("recipe.tags") }} {{ $t("tag.tags") }}
<v-icon>mdi-tag-multiple-outline</v-icon> <v-icon>mdi-tag-multiple-outline</v-icon>
</v-tab> </v-tab>
</v-tabs> </v-tabs>

View File

@ -128,8 +128,9 @@ export default {
this.requestMeals(); this.requestMeals();
}, },
async deletePlan(id) { async deletePlan(id) {
await api.mealPlans.delete(id); if (await api.mealPlans.delete(id)) {
this.requestMeals(); this.requestMeals();
}
}, },
openShoppingList(id) { openShoppingList(id) {
this.$refs.shoppingList.openDialog(id); this.$refs.shoppingList.openDialog(id);

View File

@ -52,6 +52,7 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import utils from "@/utils";
export default { export default {
data() { data() {
return { return {
@ -60,6 +61,9 @@ export default {
}, },
async mounted() { async mounted() {
this.mealPlan = await api.mealPlans.thisWeek(); this.mealPlan = await api.mealPlans.thisWeek();
if(!this.mealPlan) {
utils.notify.warning(this.$t('meal-plan.no-meal-plan-defined-yet'))
}
}, },
methods: { methods: {
getOrder(index) { getOrder(index) {

View File

@ -101,7 +101,7 @@ export default {
let slug = await api.recipes.create(this.recipeDetails); let slug = await api.recipes.create(this.recipeDetails);
if (this.fileObject) { if (this.fileObject) {
await api.recipes.updateImage(slug, this.fileObject); api.recipes.updateImage(slug, this.fileObject, true);
} }
this.isLoading = false; this.isLoading = false;

View File

@ -80,6 +80,8 @@ import RecipeEditor from "@/components/Recipe/RecipeEditor";
import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue"; import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue";
import EditorButtonRow from "@/components/Recipe/EditorButtonRow"; import EditorButtonRow from "@/components/Recipe/EditorButtonRow";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
import store from "@/store";
import { router } from "@/routes";
export default { export default {
components: { components: {
@ -166,8 +168,12 @@ export default {
return api.recipes.recipeImage(image) + "&rnd=" + this.imageKey; return api.recipes.recipeImage(image) + "&rnd=" + this.imageKey;
} }
}, },
deleteRecipe() { async deleteRecipe() {
api.recipes.delete(this.recipeDetails.slug); let response = await api.recipes.delete(this.recipeDetails.slug);
if (response) {
store.dispatch("requestRecentRecipes");
router.push(`/`);
}
}, },
validateRecipe() { validateRecipe() {
if (this.jsonEditor) { if (this.jsonEditor) {
@ -176,18 +182,19 @@ export default {
return this.$refs.recipeEditor.validateRecipe(); return this.$refs.recipeEditor.validateRecipe();
} }
}, },
async saveImage() { async saveImage(overrideSuccessMsg = false) {
if (this.fileObject) { if (this.fileObject) {
await api.recipes.updateImage(this.recipeDetails.slug, this.fileObject); if (api.recipes.updateImage(this.recipeDetails.slug, this.fileObject, overrideSuccessMsg)) {
this.imageKey += 1;
}
} }
this.imageKey += 1;
}, },
async saveRecipe() { async saveRecipe() {
if (this.validateRecipe()) { if (this.validateRecipe()) {
let slug = await api.recipes.update(this.recipeDetails); let slug = await api.recipes.update(this.recipeDetails);
if (this.fileObject) { if (this.fileObject) {
this.saveImage(); this.saveImage(true);
} }
this.form = false; this.form = false;

View File

@ -26,7 +26,7 @@
<v-row dense class="mt-0 flex-row align-center justify-space-around"> <v-row dense class="mt-0 flex-row align-center justify-space-around">
<v-col> <v-col>
<h3 class="pl-2 text-center headline"> <h3 class="pl-2 text-center headline">
{{ $t("search.category-filter") }} {{ $t("category.category-filter") }}
</h3> </h3>
<FilterSelector class="mb-1" @update="updateCatParams" /> <FilterSelector class="mb-1" @update="updateCatParams" />
<CategoryTagSelector <CategoryTagSelector

View File

@ -2,6 +2,9 @@ import Planner from "@/pages/MealPlan/Planner";
import ThisWeek from "@/pages/MealPlan/ThisWeek"; import ThisWeek from "@/pages/MealPlan/ThisWeek";
import { api } from "@/api"; import { api } from "@/api";
import i18n from '@/i18n.js';
import utils from "@/utils";
export const mealRoutes = [ export const mealRoutes = [
{ {
path: "/meal-plan/planner", path: "/meal-plan/planner",
@ -21,7 +24,12 @@ export const mealRoutes = [
path: "/meal-plan/today", path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => { beforeEnter: async (_to, _from, next) => {
await todaysMealRoute().then(redirect => { await todaysMealRoute().then(redirect => {
next(redirect); if(redirect) {
next(redirect);
} else {
utils.notify.error(i18n.t('meal-plan.no-meal-planned-for-today'));
next(_from);
}
}); });
}, },
}, },
@ -29,5 +37,9 @@ export const mealRoutes = [
async function todaysMealRoute() { async function todaysMealRoute() {
const response = await api.mealPlans.today(); const response = await api.mealPlans.today();
return "/recipe/" + response.data; if (response.status == 200 && response.data) {
return "/recipe/" + response.data;
} else {
return null;
}
} }

View File

@ -2,13 +2,12 @@ import operator
import shutil import shutil
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from mealie.core.config import app_dirs from mealie.core.config import app_dirs
from mealie.core.security import create_file_token from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, validate_file_token from mealie.routes.deps import get_current_user, validate_file_token
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.schema.snackbar import SnackResponse
from mealie.services.backups import imports from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all from mealie.services.backups.exports import backup_all
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -31,30 +30,27 @@ def available_imports():
return Imports(imports=imports, templates=templates) return Imports(imports=imports, templates=templates)
@router.post("/export/database", status_code=201) @router.post("/export/database", status_code=status.HTTP_201_CREATED)
def export_database(data: BackupJob, session: Session = Depends(generate_session)): def export_database(data: BackupJob, session: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format.""" """Generates a backup of the recipe database in json format."""
export_path = backup_all(
session=session,
tag=data.tag,
templates=data.templates,
export_recipes=data.options.recipes,
export_settings=data.options.settings,
export_pages=data.options.pages,
export_themes=data.options.themes,
export_users=data.options.users,
export_groups=data.options.groups,
)
try: try:
return SnackResponse.success("Backup Created at " + export_path) export_path = backup_all(
except: session=session,
HTTPException( tag=data.tag,
status_code=400, templates=data.templates,
detail=SnackResponse.error("Error Creating Backup. See Log File"), export_recipes=data.options.recipes,
export_settings=data.options.settings,
export_pages=data.options.pages,
export_themes=data.options.themes,
export_users=data.options.users,
export_groups=data.options.groups,
) )
return {"export_path": export_path}
except:
raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR )
@router.post("/upload") @router.post("/upload", status_code=status.HTTP_200_OK)
def upload_backup_file(archive: UploadFile = File(...)): def upload_backup_file(archive: UploadFile = File(...)):
""" Upload a .zip File to later be imported into Mealie """ """ Upload a .zip File to later be imported into Mealie """
dest = app_dirs.BACKUP_DIR.joinpath(archive.filename) dest = app_dirs.BACKUP_DIR.joinpath(archive.filename)
@ -62,10 +58,9 @@ def upload_backup_file(archive: UploadFile = File(...)):
with dest.open("wb") as buffer: with dest.open("wb") as buffer:
shutil.copyfileobj(archive.file, buffer) shutil.copyfileobj(archive.file, buffer)
if dest.is_file: if not dest.is_file:
return SnackResponse.success("Backup uploaded") raise HTTPException( status.HTTP_400_BAD_REQUEST )
else:
return SnackResponse.error("Failure uploading file")
@router.get("/{file_name}/download") @router.get("/{file_name}/download")
@ -76,7 +71,7 @@ async def download_backup_file(file_name: str):
return {"fileToken": create_file_token(file)} return {"fileToken": create_file_token(file)}
@router.post("/{file_name}/import", status_code=200) @router.post("/{file_name}/import", status_code=status.HTTP_200_OK)
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)): def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
""" Import a database backup file generated from Mealie. """ """ Import a database backup file generated from Mealie. """
@ -94,16 +89,14 @@ def import_database(file_name: str, import_data: ImportJob, session: Session = D
) )
@router.delete("/{file_name}/delete", status_code=200) @router.delete("/{file_name}/delete", status_code=status.HTTP_200_OK)
def delete_backup(file_name: str): def delete_backup(file_name: str):
""" Removes a database backup from the file system """ """ Removes a database backup from the file system """
file_path = app_dirs.BACKUP_DIR.joinpath(file_name)
if not file_path.is_file():
raise HTTPException( status.HTTP_400_BAD_REQUEST )
try: try:
app_dirs.BACKUP_DIR.joinpath(file_name).unlink() file_path.unlink()
except: except:
HTTPException( raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR )
status_code=400,
detail=SnackResponse.error("Unable to Delete Backup. See Log File"),
)
return SnackResponse.error(f"{file_name} Deleted")

View File

@ -1,8 +1,7 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, status, HTTPException
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -30,7 +29,7 @@ async def get_current_user_group(
return db.groups.get(session, current_user.group, "name") return db.groups.get(session, current_user.group, "name")
@router.post("") @router.post("", status_code=status.HTTP_201_CREATED)
async def create_group( async def create_group(
group_data: GroupBase, group_data: GroupBase,
current_user=Depends(get_current_user), current_user=Depends(get_current_user),
@ -40,9 +39,8 @@ async def create_group(
try: try:
db.groups.create(session, group_data.dict()) db.groups.create(session, group_data.dict())
return SnackResponse.success("User Group Created", {"created": True})
except: except:
return SnackResponse.error("User Group Creation Failed") raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.put("/{id}") @router.put("/{id}")
@ -55,8 +53,6 @@ async def update_group_data(
""" Updates a User Group """ """ Updates a User Group """
db.groups.update(session, id, group_data.dict()) db.groups.update(session, id, group_data.dict())
return SnackResponse.success("Group Settings Updated")
@router.delete("/{id}") @router.delete("/{id}")
async def delete_user_group( async def delete_user_group(
@ -65,16 +61,23 @@ async def delete_user_group(
""" Removes a user group from the database """ """ Removes a user group from the database """
if id == 1: if id == 1:
return SnackResponse.error("Cannot delete default group") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='DEFAULT_GROUP'
)
group: GroupInDB = db.groups.get(session, id) group: GroupInDB = db.groups.get(session, id)
if not group: if not group:
return SnackResponse.error("Group not found") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='GROUP_NOT_FOUND'
)
if not group.users == []: if not group.users == []:
return SnackResponse.error("Cannot delete group with users") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='GROUP_WITH_USERS'
)
db.groups.delete(session, id) db.groups.delete(session, id)
return

View File

@ -1,9 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.meal import MealPlanIn, MealPlanInDB from mealie.schema.meal import MealPlanIn, MealPlanInDB
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupInDB, UserInDB from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.image import image from mealie.services.image import image
from mealie.services.meal_services import get_todays_meal, process_meals from mealie.services.meal_services import get_todays_meal, process_meals
@ -23,15 +22,13 @@ def get_all_meals(
return db.groups.get_meals(session, current_user.group) return db.groups.get_meals(session, current_user.group)
@router.post("/create") @router.post("/create", status_code=status.HTTP_201_CREATED)
def create_meal_plan( def create_meal_plan(
data: MealPlanIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user) data: MealPlanIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
): ):
""" Creates a meal plan database entry """ """ Creates a meal plan database entry """
processed_plan = process_meals(session, data) processed_plan = process_meals(session, data)
db.meals.create(session, processed_plan.dict()) return db.meals.create(session, processed_plan.dict())
return SnackResponse.success("Mealplan Created")
@router.put("/{plan_id}") @router.put("/{plan_id}")
@ -44,25 +41,28 @@ def update_meal_plan(
""" Updates a meal plan based off ID """ """ Updates a meal plan based off ID """
processed_plan = process_meals(session, meal_plan) processed_plan = process_meals(session, meal_plan)
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict()) processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
db.meals.update(session, plan_id, processed_plan.dict()) try:
db.meals.update(session, plan_id, processed_plan.dict())
return SnackResponse.info("Mealplan Updated") except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.delete("/{plan_id}") @router.delete("/{plan_id}")
def delete_meal_plan(plan_id, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): def delete_meal_plan(plan_id, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
""" Removes a meal plan from the database """ """ Removes a meal plan from the database """
db.meals.delete(session, plan_id) try:
db.meals.delete(session, plan_id)
return SnackResponse.error("Mealplan Deleted") except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.get("/this-week", response_model=MealPlanInDB) @router.get("/this-week", response_model=MealPlanInDB)
def get_this_week(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)): def get_this_week(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
""" Returns the meal plan data for this week """ """ Returns the meal plan data for this week """
plans = db.groups.get_meals(session, current_user.group)
return db.groups.get_meals(session, current_user.group)[0] if plans:
return plans[0]
@router.get("/today", tags=["Meal Plan"]) @router.get("/today", tags=["Meal Plan"])
@ -74,8 +74,8 @@ def get_today(session: Session = Depends(generate_session), current_user: UserIn
group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name") group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name")
recipe = get_todays_meal(session, group_in_db) recipe = get_todays_meal(session, group_in_db)
if recipe:
return recipe.slug return recipe.slug
@router.get("/today/image", tags=["Meal Plan"]) @router.get("/today/image", tags=["Meal Plan"])
@ -90,8 +90,8 @@ def get_todays_image(session: Session = Depends(generate_session), group_name: s
if recipe: if recipe:
recipe_image = image.read_image(recipe.slug, image_type=image.IMG_OPTIONS.ORIGINAL_IMAGE) recipe_image = image.read_image(recipe.slug, image_type=image.IMG_OPTIONS.ORIGINAL_IMAGE)
else: else:
raise HTTPException(404, "no meal for today") raise HTTPException( status.HTTP_404_NOT_FOUND )
if recipe_image: if recipe_image:
return FileResponse(recipe_image) return FileResponse(recipe_image)
else: else:
raise HTTPException(404, "file not found") raise HTTPException( status.HTTP_404_NOT_FOUND )

View File

@ -2,12 +2,11 @@ import operator
import shutil import shutil
from typing import List from typing import List
from fastapi import APIRouter, Depends, File, UploadFile from fastapi import APIRouter, Depends, File, UploadFile, status
from mealie.core.config import app_dirs from mealie.core.config import app_dirs
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.migration import MigrationFile, Migrations from mealie.schema.migration import MigrationFile, Migrations
from mealie.schema.snackbar import SnackResponse
from mealie.services.migrations import migration from mealie.services.migrations import migration
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -42,7 +41,7 @@ def import_migration(import_type: migration.Migration, file_name: str, session:
return migration.migrate(import_type, file_path, session) return migration.migrate(import_type, file_path, session)
@router.delete("/{import_type}/{file_name}/delete") @router.delete("/{import_type}/{file_name}/delete", status_code=status.HTTP_200_OK)
def delete_migration_data(import_type: migration.Migration, file_name: str): def delete_migration_data(import_type: migration.Migration, file_name: str):
""" Removes migration data from the file system """ """ Removes migration data from the file system """
@ -53,12 +52,11 @@ def delete_migration_data(import_type: migration.Migration, file_name: str):
elif remove_path.is_dir(): elif remove_path.is_dir():
shutil.rmtree(remove_path) shutil.rmtree(remove_path)
else: else:
SnackResponse.error("File/Folder not found.") raise HTTPException( status.HTTP_400_BAD_REQUEST )
return SnackResponse.error(f"Migration Data Remove: {remove_path.absolute()}")
@router.post("/{import_type}/upload")
@router.post("/{import_type}/upload", status_code=status.HTTP_200_OK)
def upload_nextcloud_zipfile(import_type: migration.Migration, archive: UploadFile = File(...)): def upload_nextcloud_zipfile(import_type: migration.Migration, archive: UploadFile = File(...)):
""" Upload a .zip File to later be imported into Mealie """ """ Upload a .zip File to later be imported into Mealie """
dir = app_dirs.MIGRATION_DIR.joinpath(import_type.value) dir = app_dirs.MIGRATION_DIR.joinpath(import_type.value)
@ -68,7 +66,5 @@ def upload_nextcloud_zipfile(import_type: migration.Migration, archive: UploadFi
with dest.open("wb") as buffer: with dest.open("wb") as buffer:
shutil.copyfileobj(archive.file, buffer) shutil.copyfileobj(archive.file, buffer)
if dest.is_file: if not dest.is_file:
return SnackResponse.success("Migration data uploaded") raise HTTPException( status.HTTP_400_BAD_REQUEST )
else:
return SnackResponse.error("Failure uploading file")

View File

@ -1,9 +1,8 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.category import CategoryIn, RecipeCategoryResponse from mealie.schema.category import CategoryIn, RecipeCategoryResponse
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter( router = APIRouter(
@ -36,7 +35,10 @@ async def create_recipe_category(
): ):
""" Creates a Category in the database """ """ Creates a Category in the database """
return db.categories.create(session, category.dict()) try:
return db.categories.create(session, category.dict())
except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.put("/{category}", response_model=RecipeCategoryResponse) @router.put("/{category}", response_model=RecipeCategoryResponse)
@ -48,7 +50,10 @@ async def update_recipe_category(
): ):
""" Updates an existing Tag in the database """ """ Updates an existing Tag in the database """
return db.categories.update(session, category, new_category.dict()) try:
return db.categories.update(session, category, new_category.dict())
except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.delete("/{category}") @router.delete("/{category}")
@ -59,6 +64,7 @@ async def delete_recipe_category(
category does not impact a recipe. The category will be removed category does not impact a recipe. The category will be removed
from any recipes that contain it""" from any recipes that contain it"""
db.categories.delete(session, category) try:
db.categories.delete(session, category)
return SnackResponse.error(f"Category Deleted: {category}") except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )

View File

@ -1,13 +1,12 @@
import shutil import shutil
from fastapi import APIRouter, Depends, File, Form from fastapi import APIRouter, Depends, File, Form, status, HTTPException
from fastapi.datastructures import UploadFile from fastapi.datastructures import UploadFile
from mealie.core.config import app_dirs from mealie.core.config import app_dirs
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.recipe import Recipe, RecipeAsset from mealie.schema.recipe import Recipe, RecipeAsset
from mealie.schema.snackbar import SnackResponse
from slugify import slugify from slugify import slugify
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse from starlette.responses import FileResponse
@ -41,10 +40,10 @@ def upload_recipe_asset(
with dest.open("wb") as buffer: with dest.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer) shutil.copyfileobj(file.file, buffer)
if dest.is_file(): if not dest.is_file():
recipe: Recipe = db.recipes.get(session, recipe_slug) raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR )
recipe.assets.append(asset_in)
db.recipes.update(session, recipe_slug, recipe.dict()) recipe: Recipe = db.recipes.get(session, recipe_slug)
return asset_in recipe.assets.append(asset_in)
else: db.recipes.update(session, recipe_slug, recipe.dict())
return SnackResponse.error("Failure uploading file") return asset_in

View File

@ -1,12 +1,11 @@
from enum import Enum from enum import Enum
from fastapi import APIRouter, Depends, File, Form, HTTPException from fastapi import APIRouter, Depends, File, Form, HTTPException, status
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.recipe import Recipe, RecipeURLIn from mealie.schema.recipe import Recipe, RecipeURLIn
from mealie.schema.snackbar import SnackResponse
from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, rename_image, scrape_image, write_image from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, rename_image, scrape_image, write_image
from mealie.services.scraper.scraper import create_from_url from mealie.services.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -100,9 +99,8 @@ def delete_recipe(
db.recipes.delete(session, recipe_slug) db.recipes.delete(session, recipe_slug)
delete_image(recipe_slug) delete_image(recipe_slug)
except: except:
raise HTTPException(status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")) raise HTTPException( status.HTTP_400_BAD_REQUEST )
return SnackResponse.error(f"Recipe {recipe_slug} Deleted")
class ImageType(str, Enum): class ImageType(str, Enum):
@ -125,7 +123,7 @@ async def get_recipe_img(recipe_slug: str, image_type: ImageType = ImageType.ori
if recipe_image: if recipe_image:
return FileResponse(recipe_image) return FileResponse(recipe_image)
else: else:
raise HTTPException(404, "file not found") raise HTTPException( status.HTTP_404_NOT_FOUND )
@router.put("/{recipe_slug}/image") @router.put("/{recipe_slug}/image")
@ -152,5 +150,3 @@ def scrape_image_url(
""" Removes an existing image and replaces it with the incoming file. """ """ Removes an existing image and replaces it with the incoming file. """
scrape_image(url.url, recipe_slug) scrape_image(url.url, recipe_slug)
return SnackResponse.success("Recipe Image Updated")

View File

@ -3,7 +3,6 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.category import RecipeTagResponse, TagIn from mealie.schema.category import RecipeTagResponse, TagIn
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"]) router = APIRouter(tags=["Recipes"])
@ -58,6 +57,7 @@ async def delete_recipe_tag(
tag does not impact a recipe. The tag will be removed tag does not impact a recipe. The tag will be removed
from any recipes that contain it""" from any recipes that contain it"""
db.tags.delete(session, tag) try:
db.tags.delete(session, tag)
return SnackResponse.error(f"Tag Deleted: {tag}") except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )

View File

@ -5,7 +5,6 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.settings import CustomPageBase, CustomPageOut from mealie.schema.settings import CustomPageBase, CustomPageOut
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -29,8 +28,6 @@ async def create_new_page(
db.custom_pages.create(session, new_page.dict()) db.custom_pages.create(session, new_page.dict())
return SnackResponse.success("New Page Created")
@router.put("") @router.put("")
async def update_multiple_pages( async def update_multiple_pages(
@ -41,7 +38,6 @@ async def update_multiple_pages(
""" Update multiple custom pages """ """ Update multiple custom pages """
for page in pages: for page in pages:
db.custom_pages.update(session, page.id, page.dict()) db.custom_pages.update(session, page.id, page.dict())
return SnackResponse.success("Pages Updated")
@router.get("/{id}") @router.get("/{id}")
@ -57,7 +53,7 @@ async def get_single_page(
@router.put("/{id}") @router.put("/{id}")
async def update_single_age( async def update_single_page(
data: CustomPageOut, data: CustomPageOut,
id: int, id: int,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),

View File

@ -3,7 +3,6 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.settings import SiteSettings from mealie.schema.settings import SiteSettings
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupInDB, UserInDB from mealie.schema.user import GroupInDB, UserInDB
from mealie.utils.post_webhooks import post_webhooks from mealie.utils.post_webhooks import post_webhooks
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -27,8 +26,6 @@ def update_settings(
""" Returns Site Settings """ """ Returns Site Settings """
db.settings.update(session, 1, data.dict()) db.settings.update(session, 1, data.dict())
return SnackResponse.success("Settings Updated")
@router.post("/webhooks/test") @router.post("/webhooks/test")
def test_webhooks( def test_webhooks(

View File

@ -1,8 +1,7 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, status, HTTPException
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.theme import SiteTheme from mealie.schema.theme import SiteTheme
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -16,12 +15,11 @@ def get_all_themes(session: Session = Depends(generate_session)):
return db.themes.get_all(session) return db.themes.get_all(session)
@router.post("/themes/create") @router.post("/themes/create", status_code=status.HTTP_201_CREATED)
def create_theme(data: SiteTheme, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): def create_theme(data: SiteTheme, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
""" Creates a site color theme database entry """ """ Creates a site color theme database entry """
db.themes.create(session, data.dict()) db.themes.create(session, data.dict())
return SnackResponse.success("Theme Saved")
@router.get("/themes/{theme_name}") @router.get("/themes/{theme_name}")
@ -30,7 +28,7 @@ def get_single_theme(theme_name: str, session: Session = Depends(generate_sessio
return db.themes.get(session, theme_name) return db.themes.get(session, theme_name)
@router.put("/themes/{theme_name}") @router.put("/themes/{theme_name}", status_code=status.HTTP_200_OK)
def update_theme( def update_theme(
theme_name: str, theme_name: str,
data: SiteTheme, data: SiteTheme,
@ -40,12 +38,11 @@ def update_theme(
""" Update a theme database entry """ """ Update a theme database entry """
db.themes.update(session, theme_name, data.dict()) db.themes.update(session, theme_name, data.dict())
return SnackResponse.info(f"Theme Updated: {theme_name}")
@router.delete("/themes/{theme_name}", status_code=status.HTTP_200_OK)
@router.delete("/themes/{theme_name}")
def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
""" Deletes theme from the database """ """ Deletes theme from the database """
db.themes.delete(session, theme_name) try:
db.themes.delete(session, theme_name)
return SnackResponse.error(f"Theme Deleted: {theme_name}") except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )

View File

@ -7,7 +7,6 @@ from mealie.core import security
from mealie.core.security import authenticate_user from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -28,15 +27,11 @@ def get_token(
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token = security.create_access_token(dict(sub=email)) access_token = security.create_access_token(dict(sub=email))
return SnackResponse.success( return {"access_token": access_token, "token_type": "bearer"}
"User Successfully Logged In",
{"access_token": access_token, "token_type": "bearer"},
)
@router.get("/refresh") @router.get("/refresh")

View File

@ -1,7 +1,7 @@
import shutil import shutil
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, Depends, File, UploadFile from fastapi import APIRouter, Depends, File, UploadFile, status, HTTPException
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from mealie.core import security from mealie.core import security
from mealie.core.config import app_dirs, settings from mealie.core.config import app_dirs, settings
@ -9,7 +9,6 @@ from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -25,8 +24,7 @@ async def create_user(
new_user.password = get_password_hash(new_user.password) new_user.password = get_password_hash(new_user.password)
data = db.users.create(session, new_user.dict()) return db.users.create(session, new_user.dict())
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
@router.get("", response_model=list[UserOut]) @router.get("", response_model=list[UserOut])
@ -35,10 +33,10 @@ async def get_all_users(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
if current_user.admin: if not current_user.admin:
return db.users.get_all(session) raise HTTPException( status.HTTP_403_FORBIDDEN )
else:
return {"details": "user not authorized"} return db.users.get_all(session)
@router.get("/self", response_model=UserOut) @router.get("/self", response_model=UserOut)
@ -68,7 +66,6 @@ async def reset_user_password(
new_password = get_password_hash(settings.DEFAULT_PASSWORD) new_password = get_password_hash(settings.DEFAULT_PASSWORD)
db.users.update_password(session, id, new_password) db.users.update_password(session, id, new_password)
return SnackResponse.success("Users Password Reset")
@router.put("/{id}") @router.put("/{id}")
@ -85,8 +82,7 @@ async def update_user(
if current_user.id == id: if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email)) access_token = security.create_access_token(data=dict(sub=new_data.email))
token = {"access_token": access_token, "token_type": "bearer"} token = {"access_token": access_token, "token_type": "bearer"}
return token
return SnackResponse.success("User Updated", token)
@router.get("/{id}/image") @router.get("/{id}/image")
@ -121,10 +117,8 @@ async def update_user_image(
with dest.open("wb") as buffer: with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer) shutil.copyfileobj(profile_image.file, buffer)
if dest.is_file: if not dest.is_file:
return SnackResponse.success("File uploaded") raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR )
else:
return SnackResponse.error("Failure uploading file")
@router.put("/{id}/password") @router.put("/{id}/password")
@ -139,12 +133,12 @@ async def update_password(
match_passwords = verify_password(password_change.current_password, current_user.password) match_passwords = verify_password(password_change.current_password, current_user.password)
match_id = current_user.id == id match_id = current_user.id == id
if match_passwords and match_id: if not ( match_passwords and match_id ):
new_password = get_password_hash(password_change.new_password) raise HTTPException( status.HTTP_401_UNAUTHORIZED )
db.users.update_password(session, id, new_password)
return SnackResponse.success("Password Updated") new_password = get_password_hash(password_change.new_password)
else: db.users.update_password(session, id, new_password)
return SnackResponse.error("Existing password does not match")
@router.delete("/{id}") @router.delete("/{id}")
@ -156,8 +150,13 @@ async def delete_user(
""" Removes a user from the database. Must be the current user or a super user""" """ Removes a user from the database. Must be the current user or a super user"""
if id == 1: if id == 1:
return SnackResponse.error("Error! Cannot Delete Super User") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='SUPER_USER'
)
if current_user.id == id or current_user.admin: if current_user.id == id or current_user.admin:
db.users.delete(session, id) try:
return SnackResponse.error("User Deleted") db.users.delete(session, id)
except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )

View File

@ -6,7 +6,6 @@ from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserIn, UserInDB from mealie.schema.user import UserIn, UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -33,18 +32,16 @@ async def create_user_sign_up_key(
): ):
""" Generates a Random Token that a new user can sign up with """ """ Generates a Random Token that a new user can sign up with """
if current_user.admin: if not current_user.admin:
sign_up = { raise HTTPException( status.HTTP_403_FORBIDDEN )
"token": str(uuid.uuid1().hex),
"name": key_data.name,
"admin": key_data.admin,
}
db_entry = db.sign_ups.create(session, sign_up)
return db_entry sign_up = {
"token": str(uuid.uuid1().hex),
"name": key_data.name,
"admin": key_data.admin,
}
return db.sign_ups.create(session, sign_up)
else:
return {"details": "not authorized"}
@router.post("/{token}") @router.post("/{token}")
@ -58,7 +55,7 @@ async def create_user_with_token(
# Validate Token # Validate Token
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1) db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
if not db_entry: if not db_entry:
return SnackResponse.error("Invalid Token") raise HTTPException( status.HTTP_401_UNAUTHORIZED )
# Create User # Create User
new_user.admin = db_entry.admin new_user.admin = db_entry.admin
@ -68,9 +65,6 @@ async def create_user_with_token(
# DeleteToken # DeleteToken
db.sign_ups.delete(session, token) db.sign_ups.delete(session, token)
# Respond
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
@router.delete("/{token}") @router.delete("/{token}")
async def delete_token( async def delete_token(
@ -79,8 +73,7 @@ async def delete_token(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Removed a token from the database """ """ Removed a token from the database """
if current_user.admin: if not current_user.admin:
db.sign_ups.delete(session, token) raise HTTPException( status.HTTP_403_FORBIDDEN )
return SnackResponse.error("Sign Up Token Deleted")
else: db.sign_ups.delete(session, token)
return {"details", "not authorized"}

View File

@ -3,7 +3,6 @@ from typing import Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from mealie.routes.deps import validate_file_token from mealie.routes.deps import validate_file_token
from mealie.schema.snackbar import SnackResponse
from starlette.responses import FileResponse from starlette.responses import FileResponse
router = APIRouter(prefix="/api/utils", tags=["Utils"], include_in_schema=True) router = APIRouter(prefix="/api/utils", tags=["Utils"], include_in_schema=True)
@ -14,7 +13,7 @@ async def download_file(file_path: Optional[Path] = Depends(validate_file_token)
""" Uses a file token obtained by an active user to retrieve a file from the operating """ Uses a file token obtained by an active user to retrieve a file from the operating
system. """ system. """
print("File Name:", file_path) print("File Name:", file_path)
if file_path.is_file(): if not file_path.is_file():
return FileResponse(file_path, media_type="application/octet-stream", filename=file_path.name) raise HTTPException( status.HTTP_400_BAD_REQUEST )
else:
return SnackResponse.error("No File Found") return FileResponse(file_path, media_type="application/octet-stream", filename=file_path.name)

View File

@ -1,26 +0,0 @@
class SnackResponse:
@staticmethod
def _create_response(message: str, type: str, additional_data: dict = None) -> dict:
snackbar = {"snackbar": {"text": message, "type": type}}
if additional_data:
snackbar.update(additional_data)
return snackbar
@staticmethod
def success(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "success", additional_data)
@staticmethod
def info(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "info", additional_data)
@staticmethod
def warning(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "warning", additional_data)
@staticmethod
def error(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "error", additional_data)

View File

@ -13,12 +13,7 @@ def group_data():
def test_create_group(api_client: TestClient, api_routes: AppRoutes, token): def test_create_group(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=token) response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=token)
assert response.status_code == 200 assert response.status_code == 201
assert json.loads(response.content) == {
"snackbar": {"text": "User Group Created", "type": "success"},
"created": True,
}
def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, token): def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, token):
@ -42,8 +37,7 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, token):
# Test Update # Test Update
response = api_client.put(api_routes.groups_id(2), json=new_data, headers=token) response = api_client.put(api_routes.groups_id(2), json=new_data, headers=token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text) == {"snackbar": {"text": "Group Settings Updated", "type": "success"}}
# Validate Changes # Validate Changes
response = api_client.get(api_routes.groups, headers=token) response = api_client.get(api_routes.groups, headers=token)
all_groups = json.loads(response.text) all_groups = json.loads(response.text)
@ -51,13 +45,13 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, token):
assert next(id_2) == new_data assert next(id_2) == new_data
def test_block_delete(api_client: TestClient, api_routes: AppRoutes, token): def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.groups_id(1), headers=token) response = api_client.delete(api_routes.groups_id(1), headers=token)
assert json.loads(response.text) == {"snackbar": {"text": "Cannot delete default group", "type": "error"}} assert response.status_code == 400
def test_delete_group(api_client: TestClient, api_routes: AppRoutes, token): def test_delete_group(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.groups_id(2), headers=token) response = api_client.delete(api_routes.groups_id(2), headers=token)
assert json.loads(response.text) is None assert response.status_code == 200

View File

@ -50,7 +50,7 @@ def test_create_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1,
meal_plan = get_meal_plan_template(slug_1, slug_2) meal_plan = get_meal_plan_template(slug_1, slug_2)
response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=token) response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=token)
assert response.status_code == 200 assert response.status_code == 201
def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token): def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):

View File

@ -62,7 +62,7 @@ def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_th
def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, token): def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, token):
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token) response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
assert response.status_code == 200 assert response.status_code == 201
response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")), headers=token) response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")), headers=token)
assert response.status_code == 200 assert response.status_code == 200