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
## 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
- New Toolbox Page!
@ -38,3 +40,6 @@
### Behind the Scenes
- Unified Sidebar Components
- 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/";
import axios from "axios";
import utils from "@/utils";
import { store } from "../store";
import utils from "@/utils";
axios.defaults.headers.common[
"Authorization"
] = `Bearer ${store.getters.getToken}`;
function processResponse(response) {
try {
utils.notify.show(response.data.snackbar.text, response.data.snackbar.type);
} catch (err) {
return;
function handleError(error, getText) {
if(getText) {
utils.notify.error(getText(error.response));
}
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 = {
post: async function(url, data) {
let response = await axios.post(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return error.response;
}
});
processResponse(response);
return response;
post: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
const response = await axios.post(url, data).catch(function(error) { handleError(error, getErrorText) });
return handleResponse(response, getSuccessText);
},
put: async function(url, data) {
let response = await axios.put(url, data).catch(function(error) {
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;
put: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
const response = await axios.put(url, data).catch(function(error) { handleError(error, getErrorText) });
return handleResponse(response, getSuccessText);
},
get: async function(url, data) {
let response = await axios.get(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
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);
},
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;
get: function(url, data, getErrorText = defaultErrorText) {
return axios.get(url, data).catch(function(error) { handleError(error, getErrorText) });
},
delete: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText ) {
const response = await axios.delete(url, data).catch( function(error) { handleError(error, getErrorText) } );
return handleResponse(response, getSuccessText);
},
async download(url) {

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import { store } from "@/store";
import i18n from '@/i18n.js';
const backupBase = baseURL + "backups/";
@ -40,7 +41,12 @@ export const backupAPI = {
* @param {string} 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
@ -48,8 +54,12 @@ export const backupAPI = {
* @returns
*/
async create(options) {
let response = apiReq.post(backupURLs.createBackup, options);
return response;
return apiReq.post(
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?

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import { store } from "@/store";
import i18n from '@/i18n.js';
const prefix = baseURL + "categories";
@ -22,29 +23,44 @@ export const categoryAPI = {
return response.data;
},
async create(name) {
let response = await apiReq.post(categoryURLs.getAll, { name: name });
store.dispatch("requestCategories");
return response.data;
const response = await apiReq.post(
categoryURLs.getAll,
{ 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) {
let response = await apiReq.get(categoryURLs.getCategory(category));
return response.data;
},
async update(name, newName, overrideRequest = false) {
let response = await apiReq.put(categoryURLs.updateCategory(name), {
name: newName,
});
if (!overrideRequest) {
const response = await apiReq.put(
categoryURLs.updateCategory(name),
{ name: newName },
function() { return i18n.t('category.category-update-failed'); },
function() { return i18n.t('category.category-updated'); }
);
if (response && !overrideRequest) {
store.dispatch("requestCategories");
return response.data;
}
return response.data;
},
async delete(category, overrideRequest = false) {
let response = await apiReq.delete(categoryURLs.deleteCategory(category));
if (!overrideRequest) {
const response = await apiReq.delete(
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");
}
return response.data;
return response;
},
};
@ -68,28 +84,48 @@ export const tagAPI = {
return response.data;
},
async create(name) {
let response = await apiReq.post(tagURLs.getAll, { name: name });
store.dispatch("requestTags");
return response.data;
const response = await apiReq.post(
tagURLs.getAll,
{ 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) {
let response = await apiReq.get(tagURLs.getTag(tag));
return response.data;
},
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) {
store.dispatch("requestTags");
if(response) {
if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data;
}
return response.data;
},
async delete(tag, overrideRequest = false) {
let response = await apiReq.delete(tagURLs.deleteTag(tag));
if (!overrideRequest) {
store.dispatch("requestTags");
const response = await apiReq.delete(
tagURLs.deleteTag(tag),
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 { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const groupPrefix = baseURL + "groups";
const groupsURLs = {
@ -10,25 +11,58 @@ const groupsURLs = {
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 = {
async allGroups() {
let response = await apiReq.get(groupsURLs.groups);
return response.data;
},
async create(name) {
let response = await apiReq.post(groupsURLs.create, { name: name });
return response.data;
create(name) {
return apiReq.post(
groupsURLs.create,
{ name: name },
function() { return i18n.t('group.user-group-creation-failed'); },
function() { return i18n.t('group.user-group-created'); }
);
},
async delete(id) {
let response = await apiReq.delete(groupsURLs.delete(id));
return response.data;
delete(id) {
return apiReq.delete(
groupsURLs.delete(id),
null,
deleteErrorText,
function() { return i18n.t('group.group-deleted'); }
);
},
async current() {
let response = await apiReq.get(groupsURLs.current);
return response.data;
const response = await apiReq.get(
groupsURLs.current,
null,
null);
if(response) {
return response.data;
}
},
async update(data) {
let response = await apiReq.put(groupsURLs.update(data.id), data);
return response.data;
update(data) {
return apiReq.put(
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 { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const prefix = baseURL + "meal-plans/";
@ -15,9 +16,13 @@ const mealPlanURLs = {
};
export const mealplanAPI = {
async create(postBody) {
let response = await apiReq.post(mealPlanURLs.create, postBody);
return response;
create(postBody) {
return apiReq.post(
mealPlanURLs.create,
postBody,
function() { return i18n.t('meal-plan.mealplan-creation-failed')},
function() { return i18n.t('meal-plan.mealplan-created'); }
);
},
async all() {
@ -35,14 +40,21 @@ export const mealplanAPI = {
return response;
},
async delete(id) {
let response = await apiReq.delete(mealPlanURLs.delete(id));
return response;
delete(id) {
return apiReq.delete(mealPlanURLs.delete(id),
null,
function() { return i18n.t('meal-plan.mealplan-deletion-failed'); },
function() { return i18n.t('meal-plan.mealplan-deleted'); }
);
},
async update(id, body) {
let response = await apiReq.put(mealPlanURLs.update(id), body);
return response;
update(id, body) {
return apiReq.put(
mealPlanURLs.update(id),
body,
function() { return i18n.t('meal-plan.mealplan-update-failed'); },
function() { return i18n.t('meal-plan.mealplan-updated'); }
);
},
async shoppingList(id) {

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import { store } from "../store";
import i18n from '@/i18n.js';
const migrationBase = baseURL + "migrations";
@ -17,8 +18,13 @@ export const migrationAPI = {
return response.data;
},
async delete(folder, file) {
let response = await apiReq.delete(migrationURLs.delete(folder, file));
return response.data;
const response = await apiReq.delete(
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) {
let response = await apiReq.post(migrationURLs.import(folder, file));

View File

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

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const signUpPrefix = baseURL + "users/sign-ups";
@ -16,15 +17,25 @@ export const signupAPI = {
return response.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;
},
async deleteToken(token) {
let response = await apiReq.delete(signUpURLs.deleteToken(token));
return response.data;
return await apiReq.delete(signUpURLs.deleteToken(token),
null,
function() { return i18n.t('signup.sign-up-token-deletion-failed'); },
function() { return i18n.t('signup.sign-up-token-deleted'); }
);
},
async createUser(token, data) {
let response = await apiReq.post(signUpURLs.createUser(token), data);
return response.data;
return apiReq.post(signUpURLs.createUser(token), 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 { apiReq } from "./api-utils";
import { store } from "@/store";
import i18n from '@/i18n.js';
const settingsBase = baseURL + "site-settings";
@ -19,9 +20,16 @@ export const siteSettingsAPI = {
},
async update(body) {
let response = await apiReq.put(settingsURLs.updateSiteSettings, body);
store.dispatch("requestSiteSettings");
return response.data;
const response = await apiReq.put(
settingsURLs.updateSiteSettings,
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() {
@ -34,23 +42,39 @@ export const siteSettingsAPI = {
return response.data;
},
async createPage(body) {
let response = await apiReq.post(settingsURLs.customPages, body);
return response.data;
createPage(body) {
return apiReq.post(
settingsURLs.customPages,
body,
function() { return i18n.t('page.page-creation-failed'); },
function() { return i18n.t('page.new-page-created'); }
);
},
async deletePage(id) {
let response = await apiReq.delete(settingsURLs.customPage(id));
return response.data;
return await apiReq.delete(
settingsURLs.customPage(id),
null,
function() { return i18n.t('page.page-deletion-failed'); },
function() { return i18n.t('page.page-deleted'); });
},
async updatePage(body) {
let response = await apiReq.put(settingsURLs.customPage(body.id), body);
return response.data;
updatePage(body) {
return apiReq.put(
settingsURLs.customPage(body.id),
body,
function() { return i18n.t('page.page-update-failed'); },
function() { return i18n.t('page.page-updated'); }
);
},
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;
},
};

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import i18n from '@/i18n.js';
const prefix = baseURL + "themes";
@ -23,21 +24,31 @@ export const themeAPI = {
},
async create(postBody) {
let response = await apiReq.post(settingsURLs.createTheme, postBody);
return response.data;
return await apiReq.post(
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 = {
name: themeName,
colors: colors,
};
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
return response.data;
return apiReq.put(
settingsURLs.updateTheme(themeName),
body,
function() { return i18n.t('settings.theme.error-updating-theme'); },
function() { return i18n.t('settings.theme.theme-updated'); });
},
async delete(themeName) {
let response = await apiReq.delete(settingsURLs.deleteTheme(themeName));
return response.data;
delete(themeName) {
return apiReq.delete(
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 i18n from '@/i18n.js';
export const utilsAPI = {
// import { api } from "@/api";
async uploadFile(url, fileObject) {
uploadFile(url, fileObject) {
console.log("API Called");
let response = await apiReq.post(url, fileObject, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data;
return apiReq.post(
url,
fileObject,
function() { return i18n.t('general.failure-uploading-file'); },
function() { return i18n.t('general.file-uploaded'); }
);
},
};

View File

@ -1,6 +1,7 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import axios from "axios";
import i18n from '@/i18n.js';
const authPrefix = baseURL + "auth";
const userPrefix = baseURL + "users";
@ -17,13 +18,23 @@ const usersURLs = {
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 = {
async login(formData) {
let response = await apiReq.post(authURLs.token, formData, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
let response = await apiReq.post(
authURLs.token,
formData,
function() { return i18n.t('user.incorrect-username-or-password'); },
function() { return i18n.t('user.user-successfully-logged-in'); }
);
return response;
},
async refresh() {
@ -36,9 +47,13 @@ export const userAPI = {
let response = await apiReq.get(usersURLs.users);
return response.data;
},
async create(user) {
let response = await apiReq.post(usersURLs.users, user);
return response.data;
create(user) {
return apiReq.post(
usersURLs.users,
user,
function() { return i18n.t('user.user-creation-failed'); },
function() { return i18n.t('user.user-created'); }
);
},
async self() {
let response = await apiReq.get(usersURLs.self);
@ -48,20 +63,37 @@ export const userAPI = {
let response = await apiReq.get(usersURLs.userID(id));
return response.data;
},
async update(user) {
let response = await apiReq.put(usersURLs.userID(user.id), user);
return response.data;
update(user) {
return apiReq.put(
usersURLs.userID(user.id),
user,
function() { return i18n.t('user.user-update-failed'); },
function() { return i18n.t('user.user-updated'); }
);
},
async changePassword(id, password) {
let response = await apiReq.put(usersURLs.password(id), password);
return response.data;
changePassword(id, password) {
return apiReq.put(
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));
return response.data;
delete(id) {
return apiReq.delete(
usersURLs.userID(id),
null,
deleteErrorText,
function() { return i18n.t('user.user-deleted'); }
);
},
async resetPassword(id) {
let response = await apiReq.put(usersURLs.resetPassword(id));
return response.data;
resetPassword(id) {
return apiReq.put(
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: {
inputLabel() {
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() {
let ItemObjects = [];

View File

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

View File

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

View File

@ -13,13 +13,13 @@
class="mr-2"
>
</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-app-bar>
<v-card-text>
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.
{{$t('signup.welcome-to-mealie')}}
<v-divider class="mt-3"></v-divider>
<v-form ref="signUpForm" @submit.prevent="signUp">
<v-text-field
@ -28,7 +28,7 @@
prepend-icon="mdi-account"
validate-on-blur
:rules="[existsRule]"
label="Display Name"
:label="$t('signup.display-name')"
type="email"
></v-text-field>
<v-text-field
@ -59,7 +59,7 @@
:type="showPassword ? 'text' : 'password'"
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
:rules="[
user.password === user.passwordConfirm || 'Password must match',
user.password === user.passwordConfirm || $t('user.password-must-match'),
]"
@click:append="showPassword = !showPassword"
></v-text-field>
@ -71,11 +71,11 @@
block="block"
type="submit"
>
Sign Up
{{$t('signup.sign-up')}}
</v-btn>
</v-card-actions>
<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-form>
</v-card-text>
@ -132,18 +132,16 @@ export default {
admin: false,
};
let successUser = false;
if (this.$refs.signUpForm.validate()) {
let response = await api.signUps.createUser(this.token, userData);
successUser = response.snackbar.text.includes("Created");
if (await api.signUps.createUser(this.token, userData)) {
this.$emit("user-created");
this.$router.push("/");
}
}
this.$emit("user-created");
this.loading = false;
if (successUser) {
this.$router.push("/");
}
},
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,10 +55,10 @@ export default {
let formData = new FormData();
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.$emit(UPLOAD_EVENT);
}
},
onButtonClick() {

View File

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

View File

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

View File

@ -13,11 +13,22 @@
"demo": "Demo",
"demo-status": "Demo Status",
"development": "Development",
"download-log": "Download Log",
"download-recipe-json": "Download Recipe JSON",
"not-demo": "Not Demo",
"production": "Production",
"sqlite-file": "SQLite File",
"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": {
"apply": "Apply",
"cancel": "Cancel",
@ -30,29 +41,40 @@
"download": "Download",
"edit": "Edit",
"enabled": "Enabled",
"exception": "Exception",
"failed-count": "Failed: {count}",
"failure-uploading-file": "Failure uploading file",
"field-required": "Field Required",
"file-folder-not-found": "File/folder not found",
"file-uploaded": "File uploaded",
"filter": "Filter",
"friday": "Friday",
"get": "Get",
"groups": "Groups",
"image": "Image",
"image-upload-failed": "Image upload failed",
"import": "Import",
"keyword": "Keyword",
"monday": "Monday",
"name": "Name",
"no": "No",
"ok": "OK",
"options": "Options",
"options": "Options:",
"random": "Random",
"recent": "Recent",
"recipes": "Recipes",
"rename-object": "Rename {0}",
"reset": "Reset",
"saturday": "Saturday",
"save": "Save",
"settings": "Settings",
"sort": "Sort",
"sort-alphabetically": "A-Z",
"status": "Status",
"submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday",
"templates": "Templates",
"templates": "Templates:",
"themes": "Themes",
"thursday": "Thursday",
"token": "Token",
@ -64,6 +86,25 @@
"wednesday": "Wednesday",
"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": {
"create-a-new-meal-plan": "Create a New Meal Plan",
"dinner-this-week": "Dinner This Week",
@ -73,6 +114,14 @@
"group": "Group (Beta)",
"meal-planner": "Meal Planner",
"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",
"planner": "Planner",
"quick-week": "Quick Week",
@ -84,6 +133,7 @@
"description": "Migrate data from Chowdown",
"title": "Chowdown"
},
"migration-data-removed": "Migration data removed",
"nextcloud": {
"description": "Migrate data from a Nextcloud Cookbook intance",
"title": "Nextcloud Cookbook"
@ -102,23 +152,30 @@
"page": {
"all-recipes": "All Recipes",
"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"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"assets": "Assets",
"calories": "Calories",
"calories-suffix": "calories",
"carbohydrate-content": "Carbohydrate",
"categories": "Categories",
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
"ingredients": "Ingredients",
"instructions": "Instructions",
@ -135,27 +192,32 @@
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein",
"recipe-created": "Recipe created",
"recipe-creation-failed": "Recipe creation failed",
"recipe-deleted": "Recipe deleted",
"recipe-image": "Recipe Image",
"recipe-image-updated": "Recipe image updated",
"recipe-name": "Recipe Name",
"recipe-update-failed": "Recipe update failed",
"recipe-updated": "Recipe updated",
"servings": "Servings",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",
"unable-to-delete-recipe": "Unable to Delete Recipe",
"view-recipe": "View Recipe"
},
"search": {
"search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter",
"and": "and",
"exclude": "Exclude",
"include": "Include",
"max-results": "Max Results",
"or": "Or",
"and": "and",
"search": "Search",
"search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter"
},
"settings": {
@ -163,10 +225,15 @@
"admin-settings": "Admin Settings",
"available-backups": "Available Backups",
"backup": {
"backup-created-at-response-export_path": "Backup Created at {path}",
"backup-deleted": "Backup deleted",
"backup-tag": "Backup Tag",
"create-heading": "Create a Backup",
"error-creating-backup-see-log-file": "Error Creating Backup. See Log File",
"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-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",
"edit-page": "Edit Page",
"first-day-of-week": "First day of the week",
"group-settings-updated": "Group Settings Updated",
"homepage": {
"all-categories": "All Categories",
"card-per-section": "Card Per Section",
@ -190,9 +258,12 @@
"migrations": "Migrations",
"new-page": "New Page",
"page-name": "Page Name",
"pages": "Pages",
"profile": "Profile",
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
"set-new-time": "Set New Time",
"settings-update-failed": "Settings update failed",
"settings-updated": "Settings updated",
"site-settings": "Site Settings",
"theme": {
"accent": "Accent",
@ -203,6 +274,9 @@
"default-to-system": "Default to system",
"delete-theme": "Delete Theme",
"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",
"light": "Light",
"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.",
"success": "Success",
"theme": "Theme",
"theme-deleted": "Theme deleted",
"theme-name": "Theme Name",
"theme-name-is-required": "Theme Name is required.",
"theme-saved": "Theme Saved",
"theme-settings": "Theme Settings",
"theme-updated": "Theme updated",
"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": {
"meal-planner-webhooks": "Meal Planner 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",
"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": {
"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-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-password": "Confirm Password",
"confirm-user-deletion": "Confirm User Deletion",
"could-not-validate-credentials": "Could Not Validate Credentials",
"create-group": "Create Group",
"create-link": "Create Link",
"create-user": "Create User",
"current-password": "Current Password",
"e-mail-must-be-valid": "E-mail must be valid",
"edit-user": "Edit User",
"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",
"group": "Group",
"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",
"incorrect-username-or-password": "Incorrect username or password",
"link-id": "Link ID",
"link-name": "Link Name",
"login": "Login",
@ -262,20 +354,29 @@
"new-password": "New Password",
"new-user": "New User",
"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-reset-failed": "Password reset failed",
"password-updated": "Password updated",
"reset-password": "Reset Password",
"sign-in": "Sign in",
"sign-up-links": "Sign Up Links",
"total-mealplans": "Total MealPlans",
"total-users": "Total Users",
"upload-photo": "Upload Photo",
"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-with-value": "User ID: {id}",
"user-password": "User Password",
"user-successfully-logged-in": "User Successfully Logged In",
"user-update-failed": "User update failed",
"user-updated": "User updated",
"users": "Users",
"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-spacer></v-spacer>
<TheDownloadBtn
button-text="Download Recipe JSON"
:button-text="$t('about.download-recipe-json')"
download-url="/api/debug/last-recipe-json"
/>
<TheDownloadBtn
button-text="Download Log"
:button-text="$t('about.download-log')"
download-url="/api/debug/log"
/>
</v-card-actions>

View File

@ -62,17 +62,21 @@ export default {
},
async importBackup(data) {
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");
api.backups.delete(data.name);
this.selectedBackup = null;
if (await api.backups.delete(data.name)) {
this.selectedBackup = null;
}
this.backupLoading = false;
this.$emit("finished");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,11 +55,11 @@
>
</v-text-field>
<v-text-field
:label="$t('user.group')"
:label="$t('group.group')"
readonly
v-model="user.group"
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-form>
@ -201,11 +201,13 @@ export default {
},
async updateUser() {
this.loading = true;
let newKey = await api.users.update(this.user);
this.$store.commit("setToken", newKey.access_token);
this.refreshProfile();
this.loading = false;
this.$store.dispatch("requestUserData");
const response = await api.users.update(this.user);
if(response) {
this.$store.commit("setToken", response.data.access_token);
this.refreshProfile();
this.loading = false;
this.$store.dispatch("requestUserData");
}
},
async changePassword() {
this.paswordLoading = true;
@ -215,7 +217,9 @@ export default {
};
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;
},

View File

@ -82,14 +82,18 @@ export default {
this.$refs.categoryFormSelector.setInit(this.page.categories);
},
async submitForm() {
let response;
if (this.create) {
await api.siteSettings.createPage(this.page);
response = await api.siteSettings.createPage(this.page);
} 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;
});
await api.siteSettings.updateAllPages(this.customPages);
if (await api.siteSettings.updateAllPages(this.customPages)) {
this.getPages();
}
this.getPages();
},
editPage(index) {
this.editPageData.data = this.customPages[index];

View File

@ -223,8 +223,9 @@ export default {
this.settings.categories.splice(index, 1);
},
async saveSettings() {
await api.siteSettings.update(this.settings);
this.getOptions();
if (await api.siteSettings.update(this.settings)) {
this.getOptions();
}
},
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,9 @@ import Planner from "@/pages/MealPlan/Planner";
import ThisWeek from "@/pages/MealPlan/ThisWeek";
import { api } from "@/api";
import i18n from '@/i18n.js';
import utils from "@/utils";
export const mealRoutes = [
{
path: "/meal-plan/planner",
@ -21,7 +24,12 @@ export const mealRoutes = [
path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => {
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() {
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
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.security import create_file_token
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, validate_file_token
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.exports import backup_all
from sqlalchemy.orm.session import Session
@ -31,30 +30,27 @@ def available_imports():
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)):
"""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:
return SnackResponse.success("Backup Created at " + export_path)
except:
HTTPException(
status_code=400,
detail=SnackResponse.error("Error Creating Backup. See Log File"),
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,
)
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(...)):
""" Upload a .zip File to later be imported into Mealie """
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:
shutil.copyfileobj(archive.file, buffer)
if dest.is_file:
return SnackResponse.success("Backup uploaded")
else:
return SnackResponse.error("Failure uploading file")
if not dest.is_file:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.get("/{file_name}/download")
@ -76,7 +71,7 @@ async def download_backup_file(file_name: str):
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)):
""" 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):
""" 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:
app_dirs.BACKUP_DIR.joinpath(file_name).unlink()
file_path.unlink()
except:
HTTPException(
status_code=400,
detail=SnackResponse.error("Unable to Delete Backup. See Log File"),
)
return SnackResponse.error(f"{file_name} Deleted")
raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR )

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.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
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")
@router.post("")
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_group(
group_data: GroupBase,
current_user=Depends(get_current_user),
@ -40,9 +39,8 @@ async def create_group(
try:
db.groups.create(session, group_data.dict())
return SnackResponse.success("User Group Created", {"created": True})
except:
return SnackResponse.error("User Group Creation Failed")
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.put("/{id}")
@ -55,8 +53,6 @@ async def update_group_data(
""" Updates a User Group """
db.groups.update(session, id, group_data.dict())
return SnackResponse.success("Group Settings Updated")
@router.delete("/{id}")
async def delete_user_group(
@ -65,16 +61,23 @@ async def delete_user_group(
""" Removes a user group from the database """
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)
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 == []:
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)
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.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.meal import MealPlanIn, MealPlanInDB
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.image import image
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)
@router.post("/create")
@router.post("/create", status_code=status.HTTP_201_CREATED)
def create_meal_plan(
data: MealPlanIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
):
""" Creates a meal plan database entry """
processed_plan = process_meals(session, data)
db.meals.create(session, processed_plan.dict())
return SnackResponse.success("Mealplan Created")
return db.meals.create(session, processed_plan.dict())
@router.put("/{plan_id}")
@ -44,25 +41,28 @@ def update_meal_plan(
""" Updates a meal plan based off ID """
processed_plan = process_meals(session, meal_plan)
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
db.meals.update(session, plan_id, processed_plan.dict())
return SnackResponse.info("Mealplan Updated")
try:
db.meals.update(session, plan_id, processed_plan.dict())
except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.delete("/{plan_id}")
def delete_meal_plan(plan_id, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
""" Removes a meal plan from the database """
db.meals.delete(session, plan_id)
return SnackResponse.error("Mealplan Deleted")
try:
db.meals.delete(session, plan_id)
except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@router.get("/this-week", response_model=MealPlanInDB)
def get_this_week(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
""" Returns the meal plan data for this week """
return db.groups.get_meals(session, current_user.group)[0]
plans = db.groups.get_meals(session, current_user.group)
if plans:
return plans[0]
@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")
recipe = get_todays_meal(session, group_in_db)
return recipe.slug
if recipe:
return recipe.slug
@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:
recipe_image = image.read_image(recipe.slug, image_type=image.IMG_OPTIONS.ORIGINAL_IMAGE)
else:
raise HTTPException(404, "no meal for today")
raise HTTPException( status.HTTP_404_NOT_FOUND )
if recipe_image:
return FileResponse(recipe_image)
else:
raise HTTPException(404, "file not found")
raise HTTPException( status.HTTP_404_NOT_FOUND )

View File

@ -2,12 +2,11 @@ import operator
import shutil
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.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.migration import MigrationFile, Migrations
from mealie.schema.snackbar import SnackResponse
from mealie.services.migrations import migration
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)
@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):
""" 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():
shutil.rmtree(remove_path)
else:
SnackResponse.error("File/Folder not found.")
return SnackResponse.error(f"Migration Data Remove: {remove_path.absolute()}")
raise HTTPException( status.HTTP_400_BAD_REQUEST )
@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(...)):
""" Upload a .zip File to later be imported into Mealie """
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:
shutil.copyfileobj(archive.file, buffer)
if dest.is_file:
return SnackResponse.success("Migration data uploaded")
else:
return SnackResponse.error("Failure uploading file")
if not dest.is_file:
raise HTTPException( status.HTTP_400_BAD_REQUEST )

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.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.category import CategoryIn, RecipeCategoryResponse
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session
router = APIRouter(
@ -36,7 +35,10 @@ async def create_recipe_category(
):
""" 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)
@ -48,7 +50,10 @@ async def update_recipe_category(
):
""" 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}")
@ -59,6 +64,7 @@ async def delete_recipe_category(
category does not impact a recipe. The category will be removed
from any recipes that contain it"""
db.categories.delete(session, category)
return SnackResponse.error(f"Category Deleted: {category}")
try:
db.categories.delete(session, category)
except:
raise HTTPException( status.HTTP_400_BAD_REQUEST )

View File

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

View File

@ -1,12 +1,11 @@
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 mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
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.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session
@ -100,9 +99,8 @@ def delete_recipe(
db.recipes.delete(session, recipe_slug)
delete_image(recipe_slug)
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):
@ -125,7 +123,7 @@ async def get_recipe_img(recipe_slug: str, image_type: ImageType = ImageType.ori
if recipe_image:
return FileResponse(recipe_image)
else:
raise HTTPException(404, "file not found")
raise HTTPException( status.HTTP_404_NOT_FOUND )
@router.put("/{recipe_slug}/image")
@ -152,5 +150,3 @@ def scrape_image_url(
""" Removes an existing image and replaces it with the incoming file. """
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.routes.deps import get_current_user
from mealie.schema.category import RecipeTagResponse, TagIn
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"])
@ -58,6 +57,7 @@ async def delete_recipe_tag(
tag does not impact a recipe. The tag will be removed
from any recipes that contain it"""
db.tags.delete(session, tag)
return SnackResponse.error(f"Tag Deleted: {tag}")
try:
db.tags.delete(session, 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.routes.deps import get_current_user
from mealie.schema.settings import CustomPageBase, CustomPageOut
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
@ -29,8 +28,6 @@ async def create_new_page(
db.custom_pages.create(session, new_page.dict())
return SnackResponse.success("New Page Created")
@router.put("")
async def update_multiple_pages(
@ -41,7 +38,6 @@ async def update_multiple_pages(
""" Update multiple custom pages """
for page in pages:
db.custom_pages.update(session, page.id, page.dict())
return SnackResponse.success("Pages Updated")
@router.get("/{id}")
@ -57,7 +53,7 @@ async def get_single_page(
@router.put("/{id}")
async def update_single_age(
async def update_single_page(
data: CustomPageOut,
id: int,
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.routes.deps import get_current_user
from mealie.schema.settings import SiteSettings
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupInDB, UserInDB
from mealie.utils.post_webhooks import post_webhooks
from sqlalchemy.orm.session import Session
@ -27,8 +26,6 @@ def update_settings(
""" Returns Site Settings """
db.settings.update(session, 1, data.dict())
return SnackResponse.success("Settings Updated")
@router.post("/webhooks/test")
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.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.theme import SiteTheme
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)
@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)):
""" Creates a site color theme database entry """
db.themes.create(session, data.dict())
return SnackResponse.success("Theme Saved")
@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)
@router.put("/themes/{theme_name}")
@router.put("/themes/{theme_name}", status_code=status.HTTP_200_OK)
def update_theme(
theme_name: str,
data: SiteTheme,
@ -40,12 +38,11 @@ def update_theme(
""" Update a theme database entry """
db.themes.update(session, theme_name, data.dict())
return SnackResponse.info(f"Theme Updated: {theme_name}")
@router.delete("/themes/{theme_name}")
@router.delete("/themes/{theme_name}", status_code=status.HTTP_200_OK)
def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
""" Deletes theme from the database """
db.themes.delete(session, theme_name)
return SnackResponse.error(f"Theme Deleted: {theme_name}")
try:
db.themes.delete(session, 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.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
@ -28,15 +27,11 @@ def get_token(
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = security.create_access_token(dict(sub=email))
return SnackResponse.success(
"User Successfully Logged In",
{"access_token": access_token, "token_type": "bearer"},
)
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/refresh")

View File

@ -1,7 +1,7 @@
import shutil
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 mealie.core import security
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.db_setup import generate_session
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 sqlalchemy.orm.session import Session
@ -25,8 +24,7 @@ async def create_user(
new_user.password = get_password_hash(new_user.password)
data = db.users.create(session, new_user.dict())
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
return db.users.create(session, new_user.dict())
@router.get("", response_model=list[UserOut])
@ -35,10 +33,10 @@ async def get_all_users(
session: Session = Depends(generate_session),
):
if current_user.admin:
return db.users.get_all(session)
else:
return {"details": "user not authorized"}
if not current_user.admin:
raise HTTPException( status.HTTP_403_FORBIDDEN )
return db.users.get_all(session)
@router.get("/self", response_model=UserOut)
@ -68,7 +66,6 @@ async def reset_user_password(
new_password = get_password_hash(settings.DEFAULT_PASSWORD)
db.users.update_password(session, id, new_password)
return SnackResponse.success("Users Password Reset")
@router.put("/{id}")
@ -85,8 +82,7 @@ async def update_user(
if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email))
token = {"access_token": access_token, "token_type": "bearer"}
return SnackResponse.success("User Updated", token)
return token
@router.get("/{id}/image")
@ -121,10 +117,8 @@ async def update_user_image(
with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer)
if dest.is_file:
return SnackResponse.success("File uploaded")
else:
return SnackResponse.error("Failure uploading file")
if not dest.is_file:
raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR )
@router.put("/{id}/password")
@ -139,12 +133,12 @@ async def update_password(
match_passwords = verify_password(password_change.current_password, current_user.password)
match_id = current_user.id == id
if match_passwords and match_id:
new_password = get_password_hash(password_change.new_password)
db.users.update_password(session, id, new_password)
return SnackResponse.success("Password Updated")
else:
return SnackResponse.error("Existing password does not match")
if not ( match_passwords and match_id ):
raise HTTPException( status.HTTP_401_UNAUTHORIZED )
new_password = get_password_hash(password_change.new_password)
db.users.update_password(session, id, new_password)
@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"""
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:
db.users.delete(session, id)
return SnackResponse.error("User Deleted")
try:
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 mealie.routes.deps import get_current_user
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserIn, UserInDB
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 """
if current_user.admin:
sign_up = {
"token": str(uuid.uuid1().hex),
"name": key_data.name,
"admin": key_data.admin,
}
db_entry = db.sign_ups.create(session, sign_up)
if not current_user.admin:
raise HTTPException( status.HTTP_403_FORBIDDEN )
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}")
@ -58,7 +55,7 @@ async def create_user_with_token(
# Validate Token
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
if not db_entry:
return SnackResponse.error("Invalid Token")
raise HTTPException( status.HTTP_401_UNAUTHORIZED )
# Create User
new_user.admin = db_entry.admin
@ -68,9 +65,6 @@ async def create_user_with_token(
# DeleteToken
db.sign_ups.delete(session, token)
# Respond
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
@router.delete("/{token}")
async def delete_token(
@ -79,8 +73,7 @@ async def delete_token(
session: Session = Depends(generate_session),
):
""" Removed a token from the database """
if current_user.admin:
db.sign_ups.delete(session, token)
return SnackResponse.error("Sign Up Token Deleted")
else:
return {"details", "not authorized"}
if not current_user.admin:
raise HTTPException( status.HTTP_403_FORBIDDEN )
db.sign_ups.delete(session, token)

View File

@ -3,7 +3,6 @@ from typing import Optional
from fastapi import APIRouter, Depends
from mealie.routes.deps import validate_file_token
from mealie.schema.snackbar import SnackResponse
from starlette.responses import FileResponse
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
system. """
print("File Name:", file_path)
if file_path.is_file():
return FileResponse(file_path, media_type="application/octet-stream", filename=file_path.name)
else:
return SnackResponse.error("No File Found")
if not file_path.is_file():
raise HTTPException( status.HTTP_400_BAD_REQUEST )
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):
response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=token)
assert response.status_code == 200
assert json.loads(response.content) == {
"snackbar": {"text": "User Group Created", "type": "success"},
"created": True,
}
assert response.status_code == 201
def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, token):
@ -42,7 +37,6 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, token):
# Test Update
response = api_client.put(api_routes.groups_id(2), json=new_data, headers=token)
assert response.status_code == 200
assert json.loads(response.text) == {"snackbar": {"text": "Group Settings Updated", "type": "success"}}
# Validate Changes
response = api_client.get(api_routes.groups, headers=token)
@ -51,13 +45,13 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, token):
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)
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):
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)
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):

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):
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)
assert response.status_code == 200