Feature/submit on enter key (#224)

* general cleanup

* submit on enter

* fix signup form

* fix duplicate slugs when testing

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-03-27 13:23:08 -08:00 committed by GitHub
commit 894d6b9c9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 216 additions and 210 deletions

View file

@ -24,7 +24,7 @@
v-bind="attrs"
v-on="on"
>
{{ $t('user.create-group') }}
{{ $t("user.create-group") }}
</v-btn>
</template>
<v-card>
@ -34,31 +34,30 @@
</v-icon>
<v-toolbar-title class="headline">
{{ $t('user.create-group') }}
{{ $t("user.create-group") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text>
<v-form ref="newGroup">
<v-form ref="newGroup" @submit="createGroup">
<v-card-text>
<v-text-field
v-model="newGroupName"
:label="$t('user.group-name')"
:rules="[existsRule]"
></v-text-field>
</v-form>
</v-card-text>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="groupDialog = false">
{{ $t('general.cancel') }}
</v-btn>
<v-btn color="primary" @click="createGroup">
{{ $t('general.create') }}
</v-btn>
</v-card-actions>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="groupDialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit" @click.prevent="createGroup">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-card-actions>

View file

@ -3,7 +3,11 @@
<Confirmation
ref="deleteUserDialog"
:title="$t('user.confirm-link-deletion')"
:message="$t('user.are-you-sure-you-want-to-delete-the-link', {link: activeName })"
:message="
$t('user.are-you-sure-you-want-to-delete-the-link', {
link: activeName,
})
"
icon="mdi-alert"
@confirm="deleteUser"
:width="450"
@ -14,14 +18,14 @@
mdi-link-variant
</v-icon>
<v-toolbar-title class="headine">
{{ $t('user.sign-up-links') }}
{{ $t("user.sign-up-links") }}
</v-toolbar-title>
<v-spacer> </v-spacer>
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn small color="success" dark v-bind="attrs" v-on="on">
{{ $t('user.create-link') }}
{{ $t("user.create-link") }}
</v-btn>
</template>
<v-card>
@ -31,14 +35,13 @@
</v-icon>
<v-toolbar-title class="headline">
{{ $t('user.create-link') }}
{{ $t("user.create-link") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text>
<v-form ref="newUser">
<v-form ref="newUser" @submit="save">
<v-card-text>
<v-row class="justify-center mt-3">
<v-text-field
class="mr-2"
@ -52,18 +55,18 @@
:label="$t('user.admin')"
></v-checkbox>
</v-row>
</v-form>
</v-card-text>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="close">
{{ $t('general.cancel') }}
</v-btn>
<v-btn color="primary" @click="save">
{{ $t('general.save') }}
</v-btn>
</v-card-actions>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="close">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit" @click.prevent="save">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-toolbar>
@ -90,7 +93,7 @@
<v-icon small left>
mdi-account-cog
</v-icon>
{{ item.admin ? $t('general.yes') : $t('general.no') }}
{{ item.admin ? $t("general.yes") : $t("general.no") }}
</v-btn>
</template>
<template v-slot:item.actions="{ item }">
@ -98,7 +101,7 @@
<v-icon small left>
mdi-delete
</v-icon>
{{ $t('general.delete') }}
{{ $t("general.delete") }}
</v-btn>
</template>
</v-data-table>
@ -113,21 +116,21 @@ import { validators } from "@/mixins/validators";
export default {
components: { Confirmation },
mixins: [validators],
data() {
data() {
return {
dialog: false,
activeId: null,
activeName: null,
headers: [
{
text: this.$t('user.link-id'),
text: this.$t("user.link-id"),
align: "start",
sortable: false,
value: "id",
},
{ text: this.$t('general.name'), value: "name" },
{ text: this.$t('general.token'), value: "token" },
{ text: this.$t('user.admin'), value: "admin", align: "center" },
{ text: this.$t("general.name"), value: "name" },
{ text: this.$t("general.token"), value: "token" },
{ text: this.$t("user.admin"), value: "admin", align: "center" },
{ text: "", value: "actions", sortable: false, align: "center" },
],
links: [],
@ -144,7 +147,7 @@ export default {
admin: false,
id: 0,
},
}
};
},
computed: {

View file

@ -3,7 +3,12 @@
<Confirmation
ref="deleteUserDialog"
:title="$t('user.confirm-user-deletion')"
:message="$t('user.are-you-sure-you-want-to-delete-the-user', { activeName, activeId })"
:message="
$t('user.are-you-sure-you-want-to-delete-the-user', {
activeName,
activeId,
})
"
icon="mdi-alert"
@confirm="deleteUser"
:width="450"
@ -25,7 +30,7 @@
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn small color="success" dark v-bind="attrs" v-on="on">
{{$t('user.create-user')}}
{{ $t("user.create-user") }}
</v-btn>
</template>
<v-card>
@ -40,12 +45,11 @@
<v-spacer></v-spacer>
<v-toolbar-title class="headline">
{{$t('user.user-id-with-value', {id: editedItem.id }) }}
{{ $t("user.user-id-with-value", { id: editedItem.id }) }}
</v-toolbar-title>
</v-app-bar>
<v-card-text>
<v-form ref="newUser">
<v-form ref="newUser" @submit="save">
<v-card-text>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-text-field
@ -80,21 +84,24 @@
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="3">
<v-switch v-model="editedItem.admin" :label="$t('user.admin')"></v-switch>
<v-switch
v-model="editedItem.admin"
:label="$t('user.admin')"
></v-switch>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="close">
{{$t('general.cancel')}}
</v-btn>
<v-btn color="primary" @click="save">
{{$t('general.save')}}
</v-btn>
</v-card-actions>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="close">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit" @click.prevent="save">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-toolbar>
@ -111,13 +118,13 @@
<v-icon small left>
mdi-delete
</v-icon>
{{$t('general.delete')}}
{{ $t("general.delete") }}
</v-btn>
<v-btn small color="success" @click="editItem(item)">
<v-icon small left class="mr-2">
mdi-pencil
</v-icon>
{{$t('general.edit')}}
{{ $t("general.edit") }}
</v-btn>
</template>
<template v-slot:item.admin="{ item }">
@ -125,7 +132,7 @@
</template>
<template v-slot:no-data>
<v-btn color="primary" @click="initialize">
{{$t('general.reset')}}
{{ $t("general.reset") }}
</v-btn>
</template>
</v-data-table>
@ -140,7 +147,7 @@ import { validators } from "@/mixins/validators";
export default {
components: { Confirmation },
mixins: [validators],
data() {
data() {
return {
search: "",
dialog: false,
@ -153,10 +160,10 @@ export default {
sortable: false,
value: "id",
},
{ 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('user.admin'), value: "admin" },
{ 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("user.admin"), value: "admin" },
{ text: "", value: "actions", sortable: false, align: "center" },
],
users: [],
@ -177,12 +184,14 @@ export default {
group: "",
admin: false,
},
}
};
},
computed: {
formTitle() {
return this.editedIndex === -1 ? this.$t('user.new-user') : this.$t('user.edit-user');
return this.editedIndex === -1
? this.$t("user.new-user")
: this.$t("user.edit-user");
},
showPassword() {
return this.editedIndex === -1 ? true : false;

View file

@ -17,22 +17,24 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-card-title> </v-card-title>
<v-card-text>
<v-text-field
:label="$t('settings.theme.theme-name')"
v-model="themeName"
:rules="[rules.required]"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="dialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="success" text @click="Select" :disabled="!themeName">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
<v-form @submit="select">
<v-card-text>
<v-text-field
:label="$t('settings.theme.theme-name')"
v-model="themeName"
:rules="[rules.required]"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="dialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="success" text type="submit" @click.prevent="select" :disabled="!themeName">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
@ -64,7 +66,7 @@ export default {
randomColor() {
return "#" + Math.floor(Math.random() * 16777215).toString(16);
},
Select() {
select() {
const newTheme = {
name: this.themeName,
colors: {

View file

@ -13,11 +13,12 @@
class="mr-2"
>
</v-progress-circular>
<v-toolbar-title class="headline">{{$t('user.login')}}</v-toolbar-title>
<v-toolbar-title class="headline">{{ $t("user.login") }}</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text>
<v-form>
<v-form @submit="login">
<v-card-text>
<v-text-field
v-if="!options.isLoggingIn"
v-model="user.name"
@ -43,22 +44,24 @@
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append="showPassword = !showPassword"
></v-text-field>
</v-form>
<v-card-actions>
<v-btn
v-if="options.isLoggingIn"
@click.prevent="login"
dark
color="primary"
block="block"
type="submit"
>{{ $t("user.sign-in") }}</v-btn
>
</v-card-actions>
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
{{$t('user.could-not-validate-credentials')}}
</v-alert>
</v-card-text>
<v-card-actions>
<v-btn
v-if="options.isLoggingIn"
@click.prevent="login"
dark
color="primary"
block="block"
type="submit"
>{{ $t("user.sign-in") }}
</v-btn
>
</v-card-actions>
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
{{ $t("user.could-not-validate-credentials") }}
</v-alert>
</v-card-text>
</v-form>
</v-card>
</template>

View file

@ -21,7 +21,7 @@
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-form ref="signUpForm">
<v-form ref="signUpForm" @submit="signUp">
<v-text-field
v-model="user.name"
light="light"
@ -63,22 +63,22 @@
]"
@click:append="showPassword = !showPassword"
></v-text-field>
<v-card-actions>
<v-btn
v-if="options.isLoggingIn"
@click.prevent="signUp"
dark
color="primary"
block="block"
type="submit"
>
Sign Up
</v-btn>
</v-card-actions>
<v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error">
Error Signing Up
</v-alert>
</v-form>
<v-card-actions>
<v-btn
v-if="options.isLoggingIn"
@click.prevent="signUp"
dark
color="primary"
block="block"
type="submit"
>
Sign Up
</v-btn>
</v-card-actions>
<v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error">
Error Signing Up
</v-alert>
</v-card-text>
</v-card>
</template>

View file

@ -21,9 +21,8 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text>
<v-form ref="urlForm">
<v-form ref="urlForm" @submit="createRecipe">
<v-card-text>
<v-text-field
v-model="recipeURL"
:label="$t('new-recipe.recipe-url')"
@ -35,29 +34,29 @@
:hint="$t('new-recipe.url-form-hint')"
persistent-hint
></v-text-field>
</v-form>
<v-alert v-if="error" color="red" outlined type="success">
{{ $t("new-recipe.error-message") }}
</v-alert>
</v-card-text>
<v-alert v-if="error" color="red" outlined type="success">
{{ $t("new-recipe.error-message") }}
</v-alert>
</v-card-text>
<v-divider></v-divider>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="reset">
{{ $t("general.close") }}
</v-btn>
<v-btn
color="success"
text
@click="createRecipe"
:loading="processing"
>
{{ $t("general.submit") }}
</v-btn>
</v-card-actions>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="reset">
{{ $t("general.close") }}
</v-btn>
<v-btn
color="success"
text
@click.prevent="createRecipe"
:loading="processing"
>
{{ $t("general.submit") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
<v-speed-dial v-model="fab" fixed right bottom open-on-hover>

View file

@ -42,7 +42,6 @@ def update_generics(func):
elif attribute:
setattr(class_object, key, value)
print("Complex", complex_attributed)
func(class_object, complex_attributed)
return wrapper

View file

@ -23,7 +23,6 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
print("Login Payload", token_data)
except JWTError:
raise credentials_exception
user = db.users.get(session, token_data.username, "email")

View file

@ -77,6 +77,5 @@ def get_today(
group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name")
recipe = get_todays_meal(session, group_in_db)
print(datetime.date.today())
return recipe.slug

View file

@ -68,14 +68,11 @@ async def update_user(
token = None
if current_user.id == id or current_user.admin:
print("Current User")
db.users.update(session, id, new_data.dict())
if current_user.id == id:
print(new_data.email)
access_token = security.create_access_token(data=dict(sub=new_data.email), expires_delta=timedelta(hours=2))
token = {"access_token": access_token, "token_type": "bearer"}
if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email), expires_delta=timedelta(hours=2))
token = {"access_token": access_token, "token_type": "bearer"}
print(SnackResponse.success("User Updated", token))
return SnackResponse.success("User Updated", token)

View file

@ -33,10 +33,10 @@ class ImportJob(BackupOptions):
"example": {
"name": "my_local_backup.zip",
"recipes": True,
"force": False,
"rebase": False,
"themes": False,
"settings": False,
"settings": True,
"themes": True,
"groups": True,
"users": True,
}
}

View file

@ -89,13 +89,16 @@ class ImportDatabase:
# Migration from list to Object Type Data
try:
if "" in recipe_dict["tags"]:
recipe_dict["tags"] = [tag for tag in recipe_dict["tags"] if not tag == ""]
recipe_dict["tags"] = [tag for tag in recipe_dict["tags"] if tag != ""]
except:
pass
try:
if "" in recipe_dict["categories"]:
recipe_dict["categories"] = [cat for cat in recipe_dict["categories"] if not cat == ""]
recipe_dict["categories"] = [
cat for cat in recipe_dict["categories"] if cat != ""
]
except:
pass
@ -239,16 +242,15 @@ class ImportDatabase:
item = db_table.get(self.session, search_value, search_key)
if item:
if self.force_imports:
primary_key = getattr(item, db_table.primary_key)
db_table.delete(self.session, primary_key)
else:
if not self.force_imports:
return return_model(
name=model_name,
status=False,
exception=f"Table entry with matching '{search_key}': '{search_value}' exists",
)
primary_key = getattr(item, db_table.primary_key)
db_table.delete(self.session, primary_key)
try:
db_table.create(self.session, model.dict())
import_status = return_model(name=model_name, status=True)
@ -301,12 +303,10 @@ def import_database(
import_session.clean_up()
data = {
return {
"recipeImports": recipe_report,
"settingsImports": settings_report,
"themeImports": theme_report,
"groupImports": group_report,
"userImports": user_report,
}
return data

View file

@ -4,26 +4,16 @@ from typing import Union
import pytz
from mealie.db.database import db
from mealie.db.db_setup import create_session
from pydantic.tools import T
from mealie.schema.meal import (
MealIn,
MealOut,
MealPlanIn,
MealPlanInDB,
MealPlanProcessed,
)
from mealie.schema.meal import MealIn, MealOut, MealPlanIn, MealPlanInDB, MealPlanProcessed
from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB
from pydantic.tools import T
from sqlalchemy.orm.session import Session
def process_meals(session: Session, meal_plan_base: MealPlanIn) -> MealPlanProcessed:
meals = []
for x, meal in enumerate(meal_plan_base.meals):
# europe = pytz.timezone("America/Anchorage")
# d = europe.localize(meal_plan_base.startDate)
# print(d)
meal: MealIn
try:
recipe: Recipe = db.recipes.get(session, meal.slug)

View file

@ -2,11 +2,12 @@ import shutil
from pathlib import Path
import yaml
from fastapi.logger import logger
from mealie.core.config import IMG_DIR, TEMP_DIR
from mealie.db.database import db
from mealie.schema.recipe import Recipe
from sqlalchemy.orm.session import Session
from mealie.utils.unzip import unpack_zip
from sqlalchemy.orm.session import Session
try:
from yaml import CLoader as Loader
@ -50,11 +51,8 @@ def read_chowdown_file(recipe_file: Path) -> Recipe:
"tags": recipe_data.get("tags").split(","),
}
print(reformat_data)
reformated_list = [{"text": instruction} for instruction in reformat_data["recipeInstructions"]]
reformated_list = []
for instruction in reformat_data["recipeInstructions"]:
reformated_list.append({"text": instruction})
reformat_data["recipeInstructions"] = reformated_list
return Recipe(**reformat_data)
@ -63,21 +61,12 @@ def read_chowdown_file(recipe_file: Path) -> Recipe:
def chowdown_migrate(session: Session, zip_file: Path):
temp_dir = unpack_zip(zip_file)
print(temp_dir.name)
path = Path(temp_dir.name)
for p in path.iterdir():
print("ItterDir", p)
for p in p.iterdir():
print("Sub Itter", p)
with temp_dir as dir:
chow_dir = next(Path(dir).iterdir())
image_dir = TEMP_DIR.joinpath(chow_dir, "images")
recipe_dir = TEMP_DIR.joinpath(chow_dir, "_recipes")
print(image_dir.exists())
print(recipe_dir.exists())
failed_recipes = []
successful_recipes = []
for recipe in recipe_dir.glob("*.md"):
@ -86,17 +75,18 @@ def chowdown_migrate(session: Session, zip_file: Path):
db.recipes.create(session, new_recipe.dict())
successful_recipes.append(new_recipe.name)
except Exception as inst:
session.rollback()
logger.error(inst)
failed_recipes.append(recipe.stem)
failed_images = []
for image in image_dir.iterdir():
try:
if not image.stem in failed_recipes:
if image.stem not in failed_recipes:
shutil.copy(image, IMG_DIR.joinpath(image.name))
except Exception as inst:
print(inst)
logger.error(inst)
failed_images.append(image.name)
report = {"successful": successful_recipes, "failed": failed_recipes}
return report

View file

@ -52,8 +52,7 @@ class Cleaner:
@staticmethod
def html(raw_html):
cleanr = re.compile("<.*?>")
cleantext = re.sub(cleanr, "", raw_html)
return cleantext
return re.sub(cleanr, "", raw_html)
@staticmethod
def image(image=None) -> str:
@ -142,12 +141,11 @@ class Cleaner:
@staticmethod
def time(time_entry):
print(time_entry, type(time_entry))
if time_entry == None:
if time_entry is None:
return None
elif type(time_entry) == datetime:
print(time_entry)
elif type(time_entry) != str:
return str(time_entry)
elif time_entry != None:
else:
return time_entry

View file

@ -25,13 +25,10 @@ def create_from_url(url: str) -> Recipe:
"""
r = requests.get(url)
new_recipe = extract_recipe_from_html(r.text, url)
print(new_recipe)
new_recipe = Cleaner.clean(new_recipe, url)
new_recipe = download_image_for_recipe(new_recipe)
recipe = Recipe(**new_recipe)
return recipe
return Recipe(**new_recipe)
def extract_recipe_from_html(html: str, url: str) -> dict:

View file

@ -4,13 +4,9 @@ import requests
from fastapi.testclient import TestClient
from mealie.app import app
from mealie.core.config import SQLITE_DIR
from mealie.db.database import db
from mealie.db.db_setup import generate_session, sql_global_init
from mealie.db.init_db import init_db
from mealie.routes.deps import get_current_user
from mealie.schema.user import UserInDB
from pytest import fixture
from sqlalchemy.orm.session import Session
from tests.test_config import TEST_DATA

View file

@ -0,0 +1,25 @@
import json
import pytest
@pytest.fixture
def backup_data():
return {
"name": "dev_sample_data_2021-Feb-13.zip",
"force": False,
"recipes": True,
"settings": False, #! Broken
"themes": True,
"groups": True,
"users": True,
}
def test_import(api_client, backup_data):
response = api_client.post("/api/backups/dev_sample_data_2021-Feb-13.zip/import", json=backup_data)
assert response.status_code == 200
for key, value in json.loads(response.content).items():
for v in value:
assert v["status"] == True

View file

@ -34,6 +34,7 @@ def test_upload_chowdown_zip(api_client, chowdown_zip):
def test_import_chowdown_directory(api_client, chowdown_zip):
api_client.delete(f"{RECIPES_PREFIX}/roasted-okra") # TODO: Manage Test Data better
selection = chowdown_zip.name
response = api_client.post(f"{MIGRATIONS_PREFIX}/chowdown/{selection}/import")

View file

@ -14,6 +14,7 @@ def test_create_by_url(api_client, recipe_data: RecipeTestData):
def test_create_by_json(api_client):
api_client.delete(f"{RECIPES_PREFIX}/banana-bread")
response = api_client.post(RECIPES_CREATE, json=raw_recipe)
assert response.status_code == 201

View file

@ -66,7 +66,6 @@ def test_update_user(api_client: requests, token):
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
assert response.status_code == 200
print(response.text)
assert json.loads(response.text).get("access_token")

View file