mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feat(backend): ✨ migrate site-settings to groups (#673)
* feat(frontend): ✨ add user registration page (WIP) * feat(backend): ✨ add user registration (WIP) * test(backend): ✅ add validator testing for registration schema * feat(backend): ✨ continued work on user sign-up * feat(backend): ✨ add signup flow and user/group settings * test(backend): ✅ user-creation tests and small refactor of existing tests * fix(backend): ✅ fix failing group tests * style: 🎨 fix lint issues Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -8,6 +8,8 @@ const routes = { | ||||
|   groupsSelf: `${prefix}/groups/self`, | ||||
|   categories: `${prefix}/groups/categories`, | ||||
|  | ||||
|   preferences: `${prefix}/groups/preferences`, | ||||
|  | ||||
|   groupsId: (id: string | number) => `${prefix}/groups/${id}`, | ||||
| }; | ||||
|  | ||||
| @@ -21,13 +23,34 @@ export interface CreateGroup { | ||||
|   name: string; | ||||
| } | ||||
|  | ||||
| export interface UpdatePreferences { | ||||
|   privateGroup: boolean; | ||||
|   firstDayOfWeek: number; | ||||
|   recipePublic: boolean; | ||||
|   recipeShowNutrition: boolean; | ||||
|   recipeShowAssets: boolean; | ||||
|   recipeLandscapeView: boolean; | ||||
|   recipeDisableComments: boolean; | ||||
|   recipeDisableAmount: boolean; | ||||
| } | ||||
|  | ||||
| export interface Preferences extends UpdatePreferences { | ||||
|   id: number; | ||||
|   group_id: number; | ||||
| } | ||||
|  | ||||
| export interface Group extends CreateGroup { | ||||
|   id: number; | ||||
|   preferences: Preferences; | ||||
| } | ||||
|  | ||||
| export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> { | ||||
|   baseRoute = routes.groups; | ||||
|   itemRoute = routes.groupsId; | ||||
|   /** Returns the Group Data for the Current User | ||||
|    */ | ||||
|   async getCurrentUserGroup() { | ||||
|     return await this.requests.get(routes.groupsSelf); | ||||
|     return await this.requests.get<Group>(routes.groupsSelf); | ||||
|   } | ||||
|  | ||||
|   async getCategories() { | ||||
| @@ -37,4 +60,12 @@ export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> { | ||||
|   async setCategories(payload: Category[]) { | ||||
|     return await this.requests.put<Category[]>(routes.categories, payload); | ||||
|   } | ||||
|  | ||||
|   async getPreferences() { | ||||
|     return await this.requests.get<Preferences>(routes.preferences); | ||||
|   } | ||||
|  | ||||
|   async setPreferences(payload: UpdatePreferences) { | ||||
|     return await this.requests.put<Preferences>(routes.preferences, payload); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										25
									
								
								frontend/api/class-interfaces/user-registration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/api/class-interfaces/user-registration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { BaseAPI } from "./_base"; | ||||
|  | ||||
| export interface RegisterPayload { | ||||
|   group: string; | ||||
|   groupToken: string; | ||||
|   email: string; | ||||
|   password: string; | ||||
|   passwordConfirm: string; | ||||
|   advanced: boolean; | ||||
|   private: boolean; | ||||
| } | ||||
|  | ||||
| const prefix = "/api"; | ||||
|  | ||||
| const routes = { | ||||
|   register: `${prefix}/users/register`, | ||||
| }; | ||||
|  | ||||
| export class RegisterAPI extends BaseAPI { | ||||
|   /** Returns a list of avaiable .zip files for import into Mealie. | ||||
|    */ | ||||
|   async register(payload: RegisterPayload) { | ||||
|     return await this.requests.post<any>(routes.register, payload); | ||||
|   } | ||||
| } | ||||
| @@ -13,6 +13,7 @@ import { UnitAPI } from "./class-interfaces/recipe-units"; | ||||
| import { CookbookAPI } from "./class-interfaces/cookbooks"; | ||||
| import { WebhooksAPI } from "./class-interfaces/group-webhooks"; | ||||
| import { AdminAboutAPI } from "./class-interfaces/admin-about"; | ||||
| import { RegisterAPI } from "./class-interfaces/user-registration"; | ||||
| import { ApiRequestInstance } from "~/types/api"; | ||||
|  | ||||
| class AdminAPI { | ||||
| @@ -46,6 +47,7 @@ class Api { | ||||
|   public units: UnitAPI; | ||||
|   public cookbooks: CookbookAPI; | ||||
|   public groupWebhooks: WebhooksAPI; | ||||
|   public register: RegisterAPI; | ||||
|  | ||||
|   // Utils | ||||
|   public upload: UploadFile; | ||||
| @@ -67,6 +69,7 @@ class Api { | ||||
|     this.groups = new GroupAPI(requests); | ||||
|     this.cookbooks = new CookbookAPI(requests); | ||||
|     this.groupWebhooks = new WebhooksAPI(requests); | ||||
|     this.register = new RegisterAPI(requests); | ||||
|  | ||||
|     // Admin | ||||
|     this.events = new EventsAPI(requests); | ||||
|   | ||||
| @@ -61,13 +61,7 @@ | ||||
|                 </v-fade-transition> | ||||
|               </v-card-title> | ||||
|               <v-card-text v-if="edit"> | ||||
|                 <v-textarea | ||||
|                   :key="generateKey('instructions', index)" | ||||
|                   v-model="value[index]['text']" | ||||
|                   auto-grow | ||||
|                   dense | ||||
|                   rows="4" | ||||
|                 > | ||||
|                 <v-textarea :key="'instructions' + index" v-model="value[index]['text']" auto-grow dense rows="4"> | ||||
|                 </v-textarea> | ||||
|               </v-card-text> | ||||
|               <v-expand-transition> | ||||
|   | ||||
| @@ -134,9 +134,9 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { ref } from "@nuxtjs/composition-api"; | ||||
| import { validators } from "@/composables/use-validators"; | ||||
| import { fieldTypes } from "@/composables/forms"; | ||||
| import { ref } from "@nuxtjs/composition-api"; | ||||
|  | ||||
| const BLUR_EVENT = "blur"; | ||||
|  | ||||
|   | ||||
| @@ -138,9 +138,9 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { ref } from "@nuxtjs/composition-api"; | ||||
| import { validators } from "@/composables/use-validators"; | ||||
| import { fieldTypes } from "@/composables/forms"; | ||||
| import { ref } from "@nuxtjs/composition-api"; | ||||
|  | ||||
| const BLUR_EVENT = "blur"; | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,38 @@ import { useAsyncKey } from "./use-utils"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { CreateGroup } from "~/api/class-interfaces/groups"; | ||||
|  | ||||
| export const useGroup = function () { | ||||
| export const useGroupSelf = function () { | ||||
|   const api = useApiSingleton(); | ||||
|  | ||||
|   const actions = { | ||||
|     get() { | ||||
|       const group = useAsync(async () => { | ||||
|         const { data } = await api.groups.getCurrentUserGroup(); | ||||
|  | ||||
|         return data; | ||||
|       }, useAsyncKey()); | ||||
|  | ||||
|       return group; | ||||
|     }, | ||||
|     async updatePreferences() { | ||||
|       if (!group.value) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const { data } = await api.groups.setPreferences(group.value.preferences); | ||||
|  | ||||
|       if (data) { | ||||
|         group.value.preferences = data; | ||||
|       } | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   const group = actions.get(); | ||||
|  | ||||
|   return { actions, group }; | ||||
| }; | ||||
|  | ||||
| export const useGroupCategories = function () { | ||||
|   const api = useApiSingleton(); | ||||
|  | ||||
|   const actions = { | ||||
| @@ -61,7 +92,6 @@ export const useGroups = function () { | ||||
|   } | ||||
|  | ||||
|   async function createGroup(payload: CreateGroup) { | ||||
|     console.log(payload); | ||||
|     loading.value = true; | ||||
|     const { data } = await api.groups.createOne(payload); | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| <template> | ||||
|   <v-app dark> | ||||
|     <!-- <TheSnackbar /> --> | ||||
|  | ||||
|     <AppSidebar | ||||
|       v-model="sidebar" | ||||
|       absolute | ||||
|       :top-link="topLinks" | ||||
|       :secondary-links="$auth.user.admin ? adminLinks : null" | ||||
|       :bottom-links="$auth.user.admin ? bottomLinks : null" | ||||
|       :bottom-links="bottomLinks" | ||||
|       :user="{ data: true }" | ||||
|       :secondary-header="$t('user.admin')" | ||||
|       @input="sidebar = !sidebar" | ||||
| @@ -30,7 +27,7 @@ | ||||
|    | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import { defineComponent, ref, useContext } from "@nuxtjs/composition-api"; | ||||
| import AppHeader from "@/components/Layout/AppHeader.vue"; | ||||
| import AppSidebar from "@/components/Layout/AppSidebar.vue"; | ||||
| import TheSnackbar from "~/components/Layout/TheSnackbar.vue"; | ||||
| @@ -40,103 +37,110 @@ export default defineComponent({ | ||||
|   middleware: "auth", | ||||
|   auth: true, | ||||
|   setup() { | ||||
|     return {}; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       sidebar: null, | ||||
|       topLinks: [ | ||||
|     // @ts-ignore - $globals not found in type definition | ||||
|     const { $globals, i18n } = useContext(); | ||||
|  | ||||
|     const sidebar = ref(null); | ||||
|  | ||||
|     const topLinks = [ | ||||
|       { | ||||
|           icon: this.$globals.icons.viewDashboard, | ||||
|         icon: $globals.icons.viewDashboard, | ||||
|         to: "/admin/dashboard", | ||||
|           title: this.$t("sidebar.dashboard"), | ||||
|         title: i18n.t("sidebar.dashboard"), | ||||
|       }, | ||||
|       { | ||||
|           icon: this.$globals.icons.cog, | ||||
|         icon: $globals.icons.cog, | ||||
|         to: "/admin/site-settings", | ||||
|           title: this.$t("sidebar.site-settings"), | ||||
|         title: i18n.t("sidebar.site-settings"), | ||||
|       }, | ||||
|       { | ||||
|           icon: this.$globals.icons.tools, | ||||
|         icon: $globals.icons.tools, | ||||
|         to: "/admin/toolbox", | ||||
|           title: this.$t("sidebar.toolbox"), | ||||
|         title: i18n.t("sidebar.toolbox"), | ||||
|         children: [ | ||||
|           { | ||||
|               icon: this.$globals.icons.bellAlert, | ||||
|             icon: $globals.icons.bellAlert, | ||||
|             to: "/admin/toolbox/notifications", | ||||
|               title: this.$t("events.notification"), | ||||
|             title: i18n.t("events.notification"), | ||||
|           }, | ||||
|           { | ||||
|               icon: this.$globals.icons.foods, | ||||
|             icon: $globals.icons.foods, | ||||
|             to: "/admin/toolbox/foods", | ||||
|             title: "Manage Foods", | ||||
|           }, | ||||
|           { | ||||
|               icon: this.$globals.icons.units, | ||||
|             icon: $globals.icons.units, | ||||
|             to: "/admin/toolbox/units", | ||||
|             title: "Manage Units", | ||||
|           }, | ||||
|           { | ||||
|               icon: this.$globals.icons.tags, | ||||
|             icon: $globals.icons.tags, | ||||
|             to: "/admin/toolbox/categories", | ||||
|               title: this.$t("sidebar.tags"), | ||||
|             title: i18n.t("sidebar.tags"), | ||||
|           }, | ||||
|           { | ||||
|               icon: this.$globals.icons.tags, | ||||
|             icon: $globals.icons.tags, | ||||
|             to: "/admin/toolbox/tags", | ||||
|               title: this.$t("sidebar.categories"), | ||||
|             title: i18n.t("sidebar.categories"), | ||||
|           }, | ||||
|           { | ||||
|               icon: this.$globals.icons.broom, | ||||
|             icon: $globals.icons.broom, | ||||
|             to: "/admin/toolbox/organize", | ||||
|               title: this.$t("settings.organize"), | ||||
|             title: i18n.t("settings.organize"), | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|           icon: this.$globals.icons.group, | ||||
|         icon: $globals.icons.group, | ||||
|         to: "/admin/manage-users", | ||||
|           title: this.$t("sidebar.manage-users"), | ||||
|         title: i18n.t("sidebar.manage-users"), | ||||
|         children: [ | ||||
|           { | ||||
|               icon: this.$globals.icons.user, | ||||
|             icon: $globals.icons.user, | ||||
|             to: "/admin/manage-users/all-users", | ||||
|               title: this.$t("user.users"), | ||||
|             title: i18n.t("user.users"), | ||||
|           }, | ||||
|           { | ||||
|               icon: this.$globals.icons.group, | ||||
|             icon: $globals.icons.group, | ||||
|             to: "/admin/manage-users/all-groups", | ||||
|               title: this.$t("group.groups"), | ||||
|             title: i18n.t("group.groups"), | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|           icon: this.$globals.icons.import, | ||||
|         icon: $globals.icons.import, | ||||
|         to: "/admin/migrations", | ||||
|           title: this.$t("sidebar.migrations"), | ||||
|         title: i18n.t("sidebar.migrations"), | ||||
|       }, | ||||
|       { | ||||
|           icon: this.$globals.icons.database, | ||||
|         icon: $globals.icons.database, | ||||
|         to: "/admin/backups", | ||||
|           title: this.$t("sidebar.backups"), | ||||
|         title: i18n.t("sidebar.backups"), | ||||
|       }, | ||||
|       ], | ||||
|       bottomLinks: [ | ||||
|     ]; | ||||
|  | ||||
|     const bottomLinks = [ | ||||
|       { | ||||
|           icon: this.$globals.icons.heart, | ||||
|           title: this.$t("about.support"), | ||||
|         icon: $globals.icons.heart, | ||||
|         title: i18n.t("about.support"), | ||||
|         href: "https://github.com/sponsors/hay-kot", | ||||
|       }, | ||||
|       { | ||||
|           icon: this.$globals.icons.information, | ||||
|           title: this.$t("about.about"), | ||||
|         icon: $globals.icons.information, | ||||
|         title: i18n.t("about.about"), | ||||
|         to: "/admin/about", | ||||
|       }, | ||||
|       ], | ||||
|     ]; | ||||
|  | ||||
|     return { | ||||
|       sidebar, | ||||
|       topLinks, | ||||
|       bottomLinks, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|       <style scoped> | ||||
| </style>+ | ||||
|  | ||||
|  | ||||
|      | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <v-app dark> | ||||
|     <!-- <TheSnackbar /> --> | ||||
|     <TheSnackbar /> | ||||
|  | ||||
|     <AppHeader :menu="false"> </AppHeader> | ||||
|     <v-main> | ||||
| @@ -17,9 +17,10 @@ | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import AppFooter from "@/components/Layout/AppFooter.vue"; | ||||
| import AppHeader from "@/components/Layout/AppHeader.vue"; | ||||
| import TheSnackbar from "~/components/Layout/TheSnackbar.vue"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { AppHeader, AppFooter }, | ||||
|   components: { AppHeader, AppFooter, TheSnackbar }, | ||||
|   setup() { | ||||
|     return {}; | ||||
|   }, | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|       :top-link="topLinks" | ||||
|       secondary-header="Cookbooks" | ||||
|       :secondary-links="cookbookLinks || []" | ||||
|       :bottom-links="$auth.user.admin ? bottomLink : []" | ||||
|       :bottom-links="isAdmin ? bottomLink : []" | ||||
|       @input="sidebar = !sidebar" | ||||
|     /> | ||||
|  | ||||
| @@ -37,11 +37,13 @@ import { useCookbooks } from "~/composables/use-group-cookbooks"; | ||||
| export default defineComponent({ | ||||
|   components: { AppHeader, AppSidebar, AppFloatingButton }, | ||||
|   // @ts-ignore | ||||
|   // middleware: process.env.GLOBAL_MIDDLEWARE, | ||||
|   middleware: "auth", | ||||
|   setup() { | ||||
|     const { cookbooks } = useCookbooks(); | ||||
|     // @ts-ignore | ||||
|     const { $globals } = useContext(); | ||||
|     const { $globals, $auth } = useContext(); | ||||
|  | ||||
|     const isAdmin = computed(() => $auth.user?.admin); | ||||
|  | ||||
|     const cookbookLinks = computed(() => { | ||||
|       if (!cookbooks.value) return []; | ||||
| @@ -53,7 +55,7 @@ export default defineComponent({ | ||||
|         }; | ||||
|       }); | ||||
|     }); | ||||
|     return { cookbookLinks }; | ||||
|     return { cookbookLinks, isAdmin }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export default { | ||||
|     // https://go.nuxtjs.dev/pwa | ||||
|     "@nuxtjs/pwa", | ||||
|     // https://i18n.nuxtjs.org/setup | ||||
|     "nuxt-i18n", | ||||
|     "@nuxtjs/i18n", | ||||
|     // https://auth.nuxtjs.org/guide/setup | ||||
|     "@nuxtjs/auth-next", | ||||
|     // https://github.com/nuxt-community/proxy-module | ||||
| @@ -81,8 +81,8 @@ export default { | ||||
|  | ||||
|   auth: { | ||||
|     redirect: { | ||||
|       login: "/user/login", | ||||
|       logout: "/", | ||||
|       login: "/login", | ||||
|       logout: "/login", | ||||
|       callback: "/login", | ||||
|       home: "/", | ||||
|     }, | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
|     "@mdi/js": "^5.9.55", | ||||
|     "@nuxtjs/auth-next": "5.0.0-1624817847.21691f1", | ||||
|     "@nuxtjs/axios": "^5.13.6", | ||||
|     "@nuxtjs/i18n": "^7.0.3", | ||||
|     "@nuxtjs/proxy": "^2.1.0", | ||||
|     "@nuxtjs/pwa": "^3.3.5", | ||||
|     "@vue/composition-api": "^1.0.5", | ||||
| @@ -25,7 +26,6 @@ | ||||
|     "core-js": "^3.15.1", | ||||
|     "fuse.js": "^6.4.6", | ||||
|     "nuxt": "^2.15.7", | ||||
|     "nuxt-i18n": "^6.28.0", | ||||
|     "vuedraggable": "^2.24.3", | ||||
|     "vuetify": "^2.5.5" | ||||
|   }, | ||||
| @@ -33,7 +33,7 @@ | ||||
|     "@babel/eslint-parser": "^7.14.7", | ||||
|     "@nuxt/types": "^2.15.7", | ||||
|     "@nuxt/typescript-build": "^2.1.0", | ||||
|     "@nuxtjs/composition-api": "^0.26.0", | ||||
|     "@nuxtjs/composition-api": "^0.28.0", | ||||
|     "@nuxtjs/eslint-config-typescript": "^6.0.1", | ||||
|     "@nuxtjs/eslint-module": "^3.0.2", | ||||
|     "@nuxtjs/vuetify": "^1.12.1", | ||||
|   | ||||
| @@ -95,8 +95,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import AdminBackupImportOptions from "@/components/Domain/Admin/AdminBackupImportOptions.vue"; | ||||
| import { defineComponent, reactive, toRefs, useContext, ref } from "@nuxtjs/composition-api"; | ||||
| import AdminBackupImportOptions from "@/components/Domain/Admin/AdminBackupImportOptions.vue"; | ||||
| import { useBackups } from "~/composables/use-backups"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   | ||||
| @@ -23,8 +23,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useCookbook } from "~/composables/use-group-cookbooks"; | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   | ||||
| @@ -178,10 +178,9 @@ | ||||
|           </v-btn> | ||||
|         </v-form> | ||||
|       </v-card-text> | ||||
|       <v-btn v-if="$config.ALLOW_SIGNUP" class="mx-auto" text to="/user/sign-up"> Sign Up </v-btn> | ||||
|       <v-btn v-if="$config.ALLOW_SIGNUP" class="mx-auto" text to="/register"> Register </v-btn> | ||||
|       <v-btn v-else class="mx-auto" text disabled> Invite Only </v-btn> | ||||
|     </v-card> | ||||
|     <!-- <v-col class="fill-height"> </v-col> --> | ||||
|   </v-container> | ||||
| </template> | ||||
| 
 | ||||
							
								
								
									
										170
									
								
								frontend/pages/register.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								frontend/pages/register.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | ||||
| <template> | ||||
|   <v-container fill-height fluid class="d-flex justify-center align-start narrow-container"> | ||||
|     <v-card color="background d-flex flex-column align-center" flat width="700px"> | ||||
|       <v-card-title class="headline"> User Registration </v-card-title> | ||||
|       <v-card-text> | ||||
|         <v-form ref="domRegisterForm" @submit.prevent="register()"> | ||||
|           <ToggleState> | ||||
|             <template #activator="{ toggle }"> | ||||
|               <div class="d-flex justify-center my-2"> | ||||
|                 <v-btn-toggle tile mandatory group color="primary"> | ||||
|                   <v-btn small @click="toggle(false)"> Create a Group </v-btn> | ||||
|                   <v-btn small @click="toggle(true)"> Join a Group </v-btn> | ||||
|                 </v-btn-toggle> | ||||
|               </div> | ||||
|             </template> | ||||
|             <template #default="{ state }"> | ||||
|               <v-text-field | ||||
|                 v-if="!state" | ||||
|                 v-model="form.group" | ||||
|                 filled | ||||
|                 rounded | ||||
|                 autofocus | ||||
|                 validate-on-blur | ||||
|                 class="rounded-lg" | ||||
|                 :prepend-icon="$globals.icons.group" | ||||
|                 :rules="[tokenOrGroup]" | ||||
|                 label="New Group Name" | ||||
|               /> | ||||
|               <v-text-field | ||||
|                 v-else | ||||
|                 v-model="form.groupToken" | ||||
|                 filled | ||||
|                 rounded | ||||
|                 validate-on-blur | ||||
|                 :rules="[tokenOrGroup]" | ||||
|                 class="rounded-lg" | ||||
|                 :prepend-icon="$globals.icons.group" | ||||
|                 label="Group Token" | ||||
|               /> | ||||
|             </template> | ||||
|           </ToggleState> | ||||
|           <v-text-field | ||||
|             v-model="form.email" | ||||
|             filled | ||||
|             rounded | ||||
|             class="rounded-lg" | ||||
|             validate-on-blur | ||||
|             :prepend-icon="$globals.icons.email" | ||||
|             label="Email" | ||||
|             :rules="[validators.required, validators.email]" | ||||
|           /> | ||||
|           <v-text-field | ||||
|             v-model="form.username" | ||||
|             filled | ||||
|             rounded | ||||
|             class="rounded-lg" | ||||
|             :prepend-icon="$globals.icons.user" | ||||
|             label="Username" | ||||
|             :rules="[validators.required]" | ||||
|           /> | ||||
|           <v-text-field | ||||
|             v-model="form.password" | ||||
|             filled | ||||
|             rounded | ||||
|             class="rounded-lg" | ||||
|             :prepend-icon="$globals.icons.lock" | ||||
|             name="password" | ||||
|             label="Password" | ||||
|             type="password" | ||||
|             :rules="[validators.required]" | ||||
|           /> | ||||
|           <v-text-field | ||||
|             v-model="form.passwordConfirm" | ||||
|             filled | ||||
|             rounded | ||||
|             validate-on-blur | ||||
|             class="rounded-lg" | ||||
|             :prepend-icon="$globals.icons.lock" | ||||
|             name="password" | ||||
|             label="Confirm Password" | ||||
|             type="password" | ||||
|             :rules="[validators.required, passwordMatch]" | ||||
|           /> | ||||
|           <div class="mt-n4 px-8"> | ||||
|             <v-checkbox v-model="form.private" label="Keep My Recipes Private"></v-checkbox> | ||||
|             <p class="text-caption mt-n4"> | ||||
|               Sets your group and all recipes defaults to private. You can always change this later. | ||||
|             </p> | ||||
|             <v-checkbox v-model="form.advanced" label="Enable Advanced Content"></v-checkbox> | ||||
|             <p class="text-caption mt-n4"> | ||||
|               Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you | ||||
|               can always change this later | ||||
|             </p> | ||||
|           </div> | ||||
|           <div class="d-flex flex-column justify-center"> | ||||
|             <v-btn :loading="loggingIn" color="primary" type="submit" large rounded class="rounded-xl" block> | ||||
|               Register | ||||
|             </v-btn> | ||||
|             <v-btn class="mx-auto my-2" text to="/login"> Login </v-btn> | ||||
|           </div> | ||||
|         </v-form> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|   </v-container> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; | ||||
| import { validators } from "@/composables/use-validators"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { alert } from "~/composables/use-toast"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   layout: "basic", | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const state = reactive({ | ||||
|       loggingIn: false, | ||||
|       success: false, | ||||
|     }); | ||||
|     const allowSignup = computed(() => process.env.AllOW_SIGNUP); | ||||
|  | ||||
|     const router = useRouter(); | ||||
|  | ||||
|     // @ts-ignore | ||||
|     const domRegisterForm = ref<VForm>(null); | ||||
|  | ||||
|     const form = reactive({ | ||||
|       group: "", | ||||
|       groupToken: "", | ||||
|       email: "", | ||||
|       username: "", | ||||
|       password: "", | ||||
|       passwordConfirm: "", | ||||
|       advanced: false, | ||||
|       private: false, | ||||
|     }); | ||||
|  | ||||
|     const passwordMatch = () => form.password === form.passwordConfirm || "Passwords do not match"; | ||||
|     const tokenOrGroup = () => form.group !== "" || form.groupToken !== "" || "Group name or token must be given"; | ||||
|  | ||||
|     async function register() { | ||||
|       if (!domRegisterForm.value?.validate()) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const { data, response } = await api.register.register(form); | ||||
|  | ||||
|       if (response?.status === 201) { | ||||
|         state.success = true; | ||||
|         alert.success("Registration Success"); | ||||
|         router.push("/user/login"); | ||||
|       } | ||||
|  | ||||
|       console.log(data, response); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       domRegisterForm, | ||||
|       validators, | ||||
|       allowSignup, | ||||
|       form, | ||||
|       ...toRefs(state), | ||||
|       passwordMatch, | ||||
|       tokenOrGroup, | ||||
|       register, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| @@ -50,8 +50,8 @@ | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import { useCookbooks } from "@/composables/use-group-cookbooks"; | ||||
| import draggable from "vuedraggable"; | ||||
| import { useCookbooks } from "@/composables/use-group-cookbooks"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { draggable }, | ||||
|   | ||||
| @@ -1,39 +1,139 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <BasePageTitle divider> | ||||
|   <v-container class="narrow-container"> | ||||
|     <BasePageTitle class="mb-5"> | ||||
|       <template #header> | ||||
|         <v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img> | ||||
|       </template> | ||||
|       <template #title> Group Settings </template> | ||||
|       These items are shared within your group. Editing one of them will change it for the whole group! | ||||
|     </BasePageTitle> | ||||
|     <v-card tag="section" outlined> | ||||
|       <v-card-text> | ||||
|     <section> | ||||
|       <BaseCardSectionTitle title="Mealplan Categories"> | ||||
|         Set the categories below for the ones that you want to be included in your mealplan random generation. | ||||
|           <div class="mt-2"> | ||||
|             <BaseButton save @click="actions.updateAll()" /> | ||||
|           </div> | ||||
|       </BaseCardSectionTitle> | ||||
|       <DomainRecipeCategoryTagSelector v-if="categories" v-model="categories" /> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|       <v-card-actions> | ||||
|         <v-spacer></v-spacer> | ||||
|         <BaseButton save @click="actions.updateAll()" /> | ||||
|       </v-card-actions> | ||||
|     </section> | ||||
|  | ||||
|     <section v-if="group"> | ||||
|       <BaseCardSectionTitle class="mt-10" title="Group Preferences"></BaseCardSectionTitle> | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.privateGroup" | ||||
|         class="mt-n4" | ||||
|         label="Private Group" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|       <v-select | ||||
|         v-model="group.preferences.firstDayOfWeek" | ||||
|         :prepend-icon="$globals.icons.calendarWeekBegin" | ||||
|         :items="allDays" | ||||
|         item-text="name" | ||||
|         item-value="value" | ||||
|         :label="$t('settings.first-day-of-week')" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       /> | ||||
|     </section> | ||||
|  | ||||
|     <section v-if="group"> | ||||
|       <BaseCardSectionTitle class="mt-10" title="Default Recipe Preferences"> | ||||
|         These are the default settings when a new recipe is created in your group. These can be changed for indivdual | ||||
|         recipes in the recipe settings menu. | ||||
|       </BaseCardSectionTitle> | ||||
|  | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.recipePublic" | ||||
|         class="mt-n4" | ||||
|         label="Allow users outside of your group to see your recipes" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.recipeShowNutrition" | ||||
|         class="mt-n4" | ||||
|         label="Show nutrition information" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.recipeShowAssets" | ||||
|         class="mt-n4" | ||||
|         label="Show recipe assets" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.recipeLandscapeView" | ||||
|         class="mt-n4" | ||||
|         label="Default to landscape view" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.recipeDisableComments" | ||||
|         class="mt-n4" | ||||
|         label="Allow recipe comments from users in your group" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|       <v-checkbox | ||||
|         v-model="group.preferences.recipeDisableAmount" | ||||
|         class="mt-n4" | ||||
|         label="Enable organizing recipe ingredients by units and food" | ||||
|         @change="groupActions.updatePreferences()" | ||||
|       ></v-checkbox> | ||||
|     </section> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import { useGroup } from "~/composables/use-groups"; | ||||
| import { defineComponent, useContext } from "@nuxtjs/composition-api"; | ||||
| import { useGroupCategories, useGroupSelf } from "~/composables/use-groups"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const { categories, actions } = useGroup(); | ||||
|     const { categories, actions } = useGroupCategories(); | ||||
|     const { group, actions: groupActions } = useGroupSelf(); | ||||
|  | ||||
|     const { i18n } = useContext(); | ||||
|  | ||||
|     const allDays = [ | ||||
|       { | ||||
|         name: i18n.t("general.sunday"), | ||||
|         value: 0, | ||||
|       }, | ||||
|       { | ||||
|         name: i18n.t("general.monday"), | ||||
|         value: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: i18n.t("general.tuesday"), | ||||
|         value: 2, | ||||
|       }, | ||||
|       { | ||||
|         name: i18n.t("general.wednesday"), | ||||
|         value: 3, | ||||
|       }, | ||||
|       { | ||||
|         name: i18n.t("general.thursday"), | ||||
|         value: 4, | ||||
|       }, | ||||
|       { | ||||
|         name: i18n.t("general.friday"), | ||||
|         value: 5, | ||||
|       }, | ||||
|       { | ||||
|         name: i18n.t("general.saturday"), | ||||
|         value: 6, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     return { | ||||
|       categories, | ||||
|       actions, | ||||
|       group, | ||||
|       groupActions, | ||||
|       allDays, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|      | ||||
|  | ||||
|   | ||||
| @@ -82,6 +82,18 @@ | ||||
|         </template> | ||||
|       </ToggleState> | ||||
|     </section> | ||||
|     <section> | ||||
|       <BaseCardSectionTitle class="mt-10" title="Preferences"> </BaseCardSectionTitle> | ||||
|       <v-checkbox | ||||
|         v-model="userCopy.advanced" | ||||
|         class="mt-n4" | ||||
|         label="Show advanced features (API Keys, Webhooks, and Data Management)" | ||||
|         @change="updateUser" | ||||
|       ></v-checkbox> | ||||
|       <div class="d-flex justify-center mt-5"> | ||||
|         <v-btn outlined class="rounded-xl" to="/user/group"> Looking for Privacy Settings? </v-btn> | ||||
|       </div> | ||||
|     </section> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
|   | ||||
| @@ -27,6 +27,7 @@ | ||||
|         </v-col> | ||||
|         <v-col cols="12" sm="12" md="6"> | ||||
|           <UserProfileLinkCard | ||||
|             v-if="user.advanced" | ||||
|             :link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }" | ||||
|             :image="require('~/static/svgs/manage-api-tokens.svg')" | ||||
|           > | ||||
| @@ -63,6 +64,7 @@ | ||||
|         </v-col> | ||||
|         <v-col cols="12" sm="12" md="6"> | ||||
|           <UserProfileLinkCard | ||||
|             v-if="user.advanced" | ||||
|             :link="{ text: 'Manage Webhooks', to: '/user/group/webhooks' }" | ||||
|             :image="require('~/static/svgs/manage-webhooks.svg')" | ||||
|           > | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|       "~/*": ["./*"], | ||||
|       "@/*": ["./*"] | ||||
|     }, | ||||
|     "types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "nuxt-i18n", "@nuxtjs/auth-next"] | ||||
|     "types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "@nuxtjs/i18n", "@nuxtjs/auth-next"] | ||||
|   }, | ||||
|   "exclude": ["node_modules", ".nuxt", "dist"] | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								frontend/types/vue.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								frontend/types/vue.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| import Vue from "vue"; | ||||
| import "@nuxt/types"; | ||||
|  | ||||
| declare module "vue/types/vue" { | ||||
|  | ||||
|   interface Vue { | ||||
|     $globals: any; | ||||
|   } | ||||
| @@ -11,4 +11,7 @@ declare module "vue/types/options" { | ||||
|   interface ComponentOptions<V extends Vue> { | ||||
|     $globals?: any; | ||||
|   } | ||||
|   interface ComponentOptions<V extends UseContextReturn> { | ||||
|     $globals?: any; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1267
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										1267
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,6 +6,7 @@ from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel | ||||
| from mealie.db.models.event import Event, EventNotification | ||||
| from mealie.db.models.group import Group | ||||
| from mealie.db.models.group.cookbook import CookBook | ||||
| from mealie.db.models.group.preferences import GroupPreferencesModel | ||||
| from mealie.db.models.group.shopping_list import ShoppingList | ||||
| from mealie.db.models.group.webhooks import GroupWebhooksModel | ||||
| from mealie.db.models.mealplan import MealPlan | ||||
| @@ -20,6 +21,7 @@ from mealie.schema.admin import SiteSettings as SiteSettingsSchema | ||||
| from mealie.schema.cookbook import ReadCookBook | ||||
| from mealie.schema.events import Event as EventSchema | ||||
| from mealie.schema.events import EventNotificationIn | ||||
| from mealie.schema.group.group_preferences import ReadGroupPreferences | ||||
| from mealie.schema.group.webhook import ReadWebhook | ||||
| from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut | ||||
| from mealie.schema.recipe import ( | ||||
| @@ -89,3 +91,4 @@ class DatabaseAccessLayer: | ||||
|         self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook) | ||||
|         self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut) | ||||
|         self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook) | ||||
|         self.group_preferences = BaseAccessModel("group_id", GroupPreferencesModel, ReadGroupPreferences) | ||||
|   | ||||
| @@ -24,13 +24,11 @@ def default_recipe_unit_init(db: DatabaseAccessLayer, session: Session) -> None: | ||||
|     for unit in get_default_units(): | ||||
|         try: | ||||
|             db.ingredient_units.create(session, unit) | ||||
|             print("Ingredient Unit Created") | ||||
|         except Exception as e: | ||||
|             print(e) | ||||
|  | ||||
|     for food in get_default_foods(): | ||||
|         try: | ||||
|             db.ingredient_foods.create(session, food) | ||||
|             print("Ingredient Food Created") | ||||
|         except Exception as e: | ||||
|             print(e) | ||||
|   | ||||
| @@ -8,7 +8,9 @@ from mealie.db.database import db | ||||
| from mealie.db.db_setup import create_session, engine | ||||
| from mealie.db.models._model_base import SqlAlchemyBase | ||||
| from mealie.schema.admin import SiteSettings | ||||
| from mealie.schema.user.user import GroupBase | ||||
| from mealie.services.events import create_general_event | ||||
| from mealie.services.group_services.group_mixins import create_new_group | ||||
|  | ||||
| logger = root_logger.get_logger("init_db") | ||||
|  | ||||
| @@ -38,9 +40,8 @@ def default_settings_init(session: Session): | ||||
|  | ||||
|  | ||||
| def default_group_init(session: Session): | ||||
|     default_group = {"name": settings.DEFAULT_GROUP} | ||||
|     logger.info("Generating Default Group") | ||||
|     db.groups.create(session, default_group) | ||||
|     create_new_group(session, GroupBase(name=settings.DEFAULT_GROUP)) | ||||
|  | ||||
|  | ||||
| def default_user_init(session: Session): | ||||
|   | ||||
| @@ -82,8 +82,6 @@ def auto_init(exclude: Union[set, list] = None):  # sourcery no-metrics | ||||
|                     except Exception: | ||||
|                         get_attr = "id" | ||||
|  | ||||
|                     print(get_attr) | ||||
|  | ||||
|                     if relation_dir == ONETOMANY.name and use_list: | ||||
|                         instances = handle_one_to_many_list(get_attr, relation_cls, val) | ||||
|                         setattr(self, key, instances) | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from .._model_utils import auto_init | ||||
| from ..group.webhooks import GroupWebhooksModel | ||||
| from ..recipe.category import Category, group2categories | ||||
| from .cookbook import CookBook | ||||
| from .preferences import GroupPreferencesModel | ||||
|  | ||||
|  | ||||
| class Group(SqlAlchemyBase, BaseMixins): | ||||
| @@ -16,7 +17,15 @@ class Group(SqlAlchemyBase, BaseMixins): | ||||
|     id = sa.Column(sa.Integer, primary_key=True) | ||||
|     name = sa.Column(sa.String, index=True, nullable=False, unique=True) | ||||
|     users = orm.relationship("User", back_populates="group") | ||||
|     categories = orm.relationship(Category, secondary=group2categories, single_parent=True) | ||||
|     categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True) | ||||
|  | ||||
|     preferences = orm.relationship( | ||||
|         GroupPreferencesModel, | ||||
|         back_populates="group", | ||||
|         uselist=False, | ||||
|         single_parent=True, | ||||
|         cascade="all, delete-orphan", | ||||
|     ) | ||||
|  | ||||
|     # CRUD From Others | ||||
|     mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date") | ||||
| @@ -24,7 +33,7 @@ class Group(SqlAlchemyBase, BaseMixins): | ||||
|     cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True) | ||||
|     shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True) | ||||
|  | ||||
|     @auto_init({"users", "webhooks", "shopping_lists", "cookbooks"}) | ||||
|     @auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences"}) | ||||
|     def __init__(self, **_) -> None: | ||||
|         pass | ||||
|  | ||||
|   | ||||
							
								
								
									
										26
									
								
								mealie/db/models/group/preferences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								mealie/db/models/group/preferences.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import sqlalchemy as sa | ||||
| import sqlalchemy.orm as orm | ||||
|  | ||||
| from .._model_base import BaseMixins, SqlAlchemyBase | ||||
| from .._model_utils import auto_init | ||||
|  | ||||
|  | ||||
| class GroupPreferencesModel(SqlAlchemyBase, BaseMixins): | ||||
|     __tablename__ = "group_preferences" | ||||
|     group_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id")) | ||||
|     group = orm.relationship("Group", back_populates="preferences") | ||||
|  | ||||
|     private_group: bool = sa.Column(sa.Boolean, default=True) | ||||
|     first_day_of_week = sa.Column(sa.Integer, default=0) | ||||
|  | ||||
|     # Recipe Defaults | ||||
|     recipe_public: bool = sa.Column(sa.Boolean, default=True) | ||||
|     recipe_show_nutrition: bool = sa.Column(sa.Boolean, default=False) | ||||
|     recipe_show_assets: bool = sa.Column(sa.Boolean, default=False) | ||||
|     recipe_landscape_view: bool = sa.Column(sa.Boolean, default=False) | ||||
|     recipe_disable_comments: bool = sa.Column(sa.Boolean, default=False) | ||||
|     recipe_disable_amount: bool = sa.Column(sa.Boolean, default=False) | ||||
|  | ||||
|     @auto_init() | ||||
|     def __init__(self, **_) -> None: | ||||
|         pass | ||||
| @@ -28,6 +28,7 @@ class User(SqlAlchemyBase, BaseMixins): | ||||
|     email = Column(String, unique=True, index=True) | ||||
|     password = Column(String) | ||||
|     admin = Column(Boolean, default=False) | ||||
|     advanced = Column(Boolean, default=False) | ||||
|  | ||||
|     group_id = Column(Integer, ForeignKey("groups.id")) | ||||
|     group = orm.relationship("Group", back_populates="users") | ||||
| @@ -51,6 +52,7 @@ class User(SqlAlchemyBase, BaseMixins): | ||||
|         favorite_recipes: list[str] = None, | ||||
|         group: str = settings.DEFAULT_GROUP, | ||||
|         admin=False, | ||||
|         advanced=False, | ||||
|         **_ | ||||
|     ) -> None: | ||||
|  | ||||
| @@ -61,6 +63,7 @@ class User(SqlAlchemyBase, BaseMixins): | ||||
|         self.group = Group.get_ref(session, group) | ||||
|         self.admin = admin | ||||
|         self.password = password | ||||
|         self.advanced = advanced | ||||
|  | ||||
|         self.favorite_recipes = [ | ||||
|             RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes | ||||
| @@ -69,13 +72,26 @@ class User(SqlAlchemyBase, BaseMixins): | ||||
|         if self.username is None: | ||||
|             self.username = full_name | ||||
|  | ||||
|     def update(self, full_name, email, group, admin, username, session=None, favorite_recipes=None, password=None, **_): | ||||
|     def update( | ||||
|         self, | ||||
|         full_name, | ||||
|         email, | ||||
|         group, | ||||
|         admin, | ||||
|         username, | ||||
|         session=None, | ||||
|         favorite_recipes=None, | ||||
|         password=None, | ||||
|         advanced=False, | ||||
|         **_ | ||||
|     ): | ||||
|         favorite_recipes = favorite_recipes or [] | ||||
|         self.username = username | ||||
|         self.full_name = full_name | ||||
|         self.email = email | ||||
|         self.group = Group.get_ref(session, group) | ||||
|         self.admin = admin | ||||
|         self.advanced = advanced | ||||
|  | ||||
|         if self.username is None: | ||||
|             self.username = full_name | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import operator | ||||
| import shutil | ||||
| from pathlib import Path | ||||
| from pprint import pprint | ||||
|  | ||||
| from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status | ||||
| from sqlalchemy.orm.session import Session | ||||
| @@ -97,8 +96,6 @@ def import_database( | ||||
|         rebase=import_data.rebase, | ||||
|     ) | ||||
|  | ||||
|     pprint(db_import) | ||||
|  | ||||
|     background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session) | ||||
|     return db_import | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| from fastapi import APIRouter | ||||
|  | ||||
| from mealie.services.base_http_service import RouterFactory | ||||
| from mealie.services.cookbook.cookbook_service import CookbookService | ||||
| from mealie.services.group.webhook_service import WebhookService | ||||
| from mealie.services._base_http_service import RouterFactory | ||||
| from mealie.services.group_services import CookbookService, WebhookService | ||||
|  | ||||
| from . import categories, crud, self_service | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ from fastapi import Depends | ||||
|  | ||||
| from mealie.routes.routers import UserAPIRouter | ||||
| from mealie.schema.recipe.recipe_category import CategoryBase | ||||
| from mealie.services.group.group_service import GroupSelfService | ||||
| from mealie.services.group_services.group_service import GroupSelfService | ||||
|  | ||||
| user_router = UserAPIRouter(prefix="/groups/categories", tags=["Groups: Mealplan Categories"]) | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,27 @@ | ||||
| from fastapi import Depends | ||||
|  | ||||
| from mealie.routes.routers import UserAPIRouter | ||||
| from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences | ||||
| from mealie.schema.user.user import GroupInDB | ||||
| from mealie.services.group.group_service import GroupSelfService | ||||
| from mealie.services.group_services.group_service import GroupSelfService | ||||
|  | ||||
| user_router = UserAPIRouter(prefix="/groups/self", tags=["Groups: Self Service"]) | ||||
| user_router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"]) | ||||
|  | ||||
|  | ||||
| @user_router.get("", response_model=GroupInDB) | ||||
| async def get_logged_in_user_group(g_self_service: GroupSelfService = Depends(GroupSelfService.write_existing)): | ||||
| @user_router.get("/self", response_model=GroupInDB) | ||||
| async def get_logged_in_user_group(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)): | ||||
|     """ Returns the Group Data for the Current User """ | ||||
|  | ||||
|     return g_self_service.item | ||||
|     return g_service.item | ||||
|  | ||||
|  | ||||
| @user_router.put("/preferences", response_model=ReadGroupPreferences) | ||||
| def update_group_preferences( | ||||
|     new_pref: UpdateGroupPreferences, g_service: GroupSelfService = Depends(GroupSelfService.write_existing) | ||||
| ): | ||||
|     return g_service.update_preferences(new_pref).preferences | ||||
|  | ||||
|  | ||||
| @user_router.get("/preferences", response_model=ReadGroupPreferences) | ||||
| def get_group_preferences(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)): | ||||
|     return g_service.item.preferences | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| from fastapi import APIRouter | ||||
|  | ||||
| from . import api_tokens, crud, favorites, images, passwords, sign_up | ||||
| from . import api_tokens, crud, favorites, images, passwords, registration, sign_up | ||||
|  | ||||
| # Must be used because of the way FastAPI works with nested routes | ||||
| user_prefix = "/users" | ||||
|  | ||||
| router = APIRouter() | ||||
|  | ||||
| router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"]) | ||||
|  | ||||
| router.include_router(sign_up.admin_router, prefix=user_prefix, tags=["Users: Sign-Up"]) | ||||
| router.include_router(sign_up.public_router, prefix=user_prefix, tags=["Users: Sign-Up"]) | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ from mealie.db.database import db | ||||
| from mealie.db.db_setup import generate_session | ||||
| from mealie.routes.routers import UserAPIRouter | ||||
| from mealie.schema.user import ChangePassword | ||||
| from mealie.services.user.user_service import UserService | ||||
| from mealie.services.user_services import UserService | ||||
|  | ||||
| user_router = UserAPIRouter(prefix="") | ||||
|  | ||||
|   | ||||
							
								
								
									
										14
									
								
								mealie/routes/users/registration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								mealie/routes/users/registration.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| from fastapi import APIRouter, Depends, status | ||||
|  | ||||
| from mealie.schema.user.registration import CreateUserRegistration | ||||
| from mealie.schema.user.user import UserOut | ||||
| from mealie.services.user_services.registration_service import RegistrationService | ||||
|  | ||||
| router = APIRouter(prefix="/register") | ||||
|  | ||||
|  | ||||
| @router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) | ||||
| def reset_user_password( | ||||
|     data: CreateUserRegistration, registration_service: RegistrationService = Depends(RegistrationService.public) | ||||
| ): | ||||
|     return registration_service.register_user(data) | ||||
							
								
								
									
										25
									
								
								mealie/schema/group/group_preferences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								mealie/schema/group/group_preferences.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| from fastapi_camelcase import CamelModel | ||||
|  | ||||
|  | ||||
| class UpdateGroupPreferences(CamelModel): | ||||
|     private_group: bool = False | ||||
|     first_day_of_week: int = 0 | ||||
|  | ||||
|     # Recipe Defaults | ||||
|     recipe_public: bool = True | ||||
|     recipe_show_nutrition: bool = False | ||||
|     recipe_show_assets: bool = False | ||||
|     recipe_landscape_view: bool = False | ||||
|     recipe_disable_comments: bool = False | ||||
|     recipe_disable_amount: bool = False | ||||
|  | ||||
|  | ||||
| class CreateGroupPreferences(UpdateGroupPreferences): | ||||
|     group_id: int | ||||
|  | ||||
|  | ||||
| class ReadGroupPreferences(CreateGroupPreferences): | ||||
|     id: int | ||||
|  | ||||
|     class Config: | ||||
|         orm_mode = True | ||||
							
								
								
									
										29
									
								
								mealie/schema/user/registration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								mealie/schema/user/registration.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| from fastapi_camelcase import CamelModel | ||||
| from pydantic import validator | ||||
| from pydantic.types import constr | ||||
|  | ||||
|  | ||||
| class CreateUserRegistration(CamelModel): | ||||
|     group: str = None | ||||
|     group_token: str = None | ||||
|     email: constr(to_lower=True, strip_whitespace=True) | ||||
|     username: constr(to_lower=True, strip_whitespace=True) | ||||
|     password: str | ||||
|     password_confirm: str | ||||
|     advanced: bool = False | ||||
|     private: bool = False | ||||
|  | ||||
|     @validator("password_confirm") | ||||
|     @classmethod | ||||
|     def passwords_match(cls, value, values): | ||||
|         if "password" in values and value != values["password"]: | ||||
|             raise ValueError("passwords do not match") | ||||
|         return value | ||||
|  | ||||
|     @validator("group_token", always=True) | ||||
|     @classmethod | ||||
|     def group_or_token(cls, value, values): | ||||
|         if bool(value) is False and bool(values["group"]) is False: | ||||
|             raise ValueError("group or group_token must be provided") | ||||
|  | ||||
|         return value | ||||
| @@ -6,8 +6,8 @@ from pydantic.types import constr | ||||
| from pydantic.utils import GetterDict | ||||
|  | ||||
| from mealie.core.config import settings | ||||
| from mealie.db.models.group import Group | ||||
| from mealie.db.models.users import User | ||||
| from mealie.schema.group.group_preferences import ReadGroupPreferences | ||||
| from mealie.schema.recipe import RecipeSummary | ||||
|  | ||||
| from ..meal_plan import MealPlanOut, ShoppingListOut | ||||
| @@ -50,8 +50,9 @@ class UserBase(CamelModel): | ||||
|     username: Optional[str] | ||||
|     full_name: Optional[str] = None | ||||
|     email: constr(to_lower=True, strip_whitespace=True) | ||||
|     admin: bool | ||||
|     admin: bool = False | ||||
|     group: Optional[str] | ||||
|     advanced: bool = False | ||||
|     favorite_recipes: Optional[list[str]] = [] | ||||
|  | ||||
|     class Config: | ||||
| @@ -128,16 +129,11 @@ class GroupInDB(UpdateGroup): | ||||
|     users: Optional[list[UserOut]] | ||||
|     mealplans: Optional[list[MealPlanOut]] | ||||
|     shopping_lists: Optional[list[ShoppingListOut]] | ||||
|     preferences: Optional[ReadGroupPreferences] = None | ||||
|  | ||||
|     class Config: | ||||
|         orm_mode = True | ||||
|  | ||||
|         @classmethod | ||||
|         def getter_dict(_cls, orm_model: Group): | ||||
|             return { | ||||
|                 **GetterDict(orm_model), | ||||
|             } | ||||
|  | ||||
|  | ||||
| class LongLiveTokenInDB(CreateToken): | ||||
|     id: int | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| from .cookbook_service import * | ||||
| @@ -1,2 +1,3 @@ | ||||
| from .cookbook_service import * | ||||
| from .group_service import * | ||||
| from .webhook_service import * | ||||
| @@ -3,7 +3,7 @@ from __future__ import annotations | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.db.database import get_database | ||||
| from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services._base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_group_event | ||||
| from mealie.utils.error_messages import ErrorMessages | ||||
| 
 | ||||
							
								
								
									
										16
									
								
								mealie/services/group_services/group_mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mealie/services/group_services/group_mixins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| from mealie.db.database import get_database | ||||
| from mealie.schema.group.group_preferences import CreateGroupPreferences | ||||
| from mealie.schema.user.user import GroupBase, GroupInDB | ||||
|  | ||||
|  | ||||
| def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB: | ||||
|     db = get_database() | ||||
|     created_group = db.groups.create(session, g_base) | ||||
|  | ||||
|     g_preferences = g_preferences or CreateGroupPreferences(group_id=0) | ||||
|  | ||||
|     g_preferences.group_id = created_group.id | ||||
|  | ||||
|     db.group_preferences.create(session, g_preferences) | ||||
|  | ||||
|     return created_group | ||||
| @@ -4,9 +4,10 @@ from fastapi import Depends, HTTPException, status | ||||
| 
 | ||||
| from mealie.core.dependencies.grouped import UserDeps | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.schema.group.group_preferences import UpdateGroupPreferences | ||||
| from mealie.schema.recipe.recipe_category import CategoryBase | ||||
| from mealie.schema.user.user import GroupInDB | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services._base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_group_event | ||||
| 
 | ||||
| logger = get_logger(module=__name__) | ||||
| @@ -41,8 +42,11 @@ class GroupSelfService(UserHttpService[int, str]): | ||||
|         return self.item | ||||
| 
 | ||||
|     def update_categories(self, new_categories: list[CategoryBase]): | ||||
|         if not self.item: | ||||
|             return | ||||
|         self.item.categories = new_categories | ||||
| 
 | ||||
|         return self.db.groups.update(self.session, self.group_id, self.item) | ||||
| 
 | ||||
|     def update_preferences(self, new_preferences: UpdateGroupPreferences): | ||||
|         self.db.group_preferences.update(self.session, self.group_id, new_preferences) | ||||
| 
 | ||||
|         return self.populate_item() | ||||
| @@ -5,7 +5,7 @@ from fastapi import HTTPException, status | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.schema.group import ReadWebhook | ||||
| from mealie.schema.group.webhook import CreateWebhook, SaveWebhook | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services._base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_group_event | ||||
| 
 | ||||
| logger = get_logger(module=__name__) | ||||
| @@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError | ||||
| from mealie.core.dependencies.grouped import PublicDeps, UserDeps | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.schema.recipe.recipe import CreateRecipe, Recipe | ||||
| from mealie.services.base_http_service.http_services import PublicHttpService | ||||
| from mealie.services._base_http_service.http_services import PublicHttpService | ||||
| from mealie.services.events import create_recipe_event | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|   | ||||
| @@ -55,7 +55,6 @@ def _exec_crf_test(input_text): | ||||
|  | ||||
|  | ||||
| def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]): | ||||
|     print(list_of_ingrdeint_text) | ||||
|     crf_output = _exec_crf_test([pre_process_string(x) for x in list_of_ingrdeint_text]) | ||||
|     crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))] | ||||
|  | ||||
| @@ -82,6 +81,3 @@ def convert_crf_models_to_ingredients(crf_models: list[CRFIngredient]): | ||||
| if __name__ == "__main__": | ||||
|     crf_models = convert_list_to_crf_model(INGREDIENT_TEXT) | ||||
|     ingredients = convert_crf_models_to_ingredients(crf_models) | ||||
|  | ||||
|     for ingredient in ingredients: | ||||
|         print(ingredient.input) | ||||
|   | ||||
							
								
								
									
										1
									
								
								mealie/services/user_services/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								mealie/services/user_services/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| from .user_service import * | ||||
							
								
								
									
										61
									
								
								mealie/services/user_services/registration_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								mealie/services/user_services/registration_service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.core.security import hash_password | ||||
| from mealie.schema.group.group_preferences import CreateGroupPreferences | ||||
| from mealie.schema.user.registration import CreateUserRegistration | ||||
| from mealie.schema.user.user import GroupBase, GroupInDB, PrivateUser, UserIn | ||||
| from mealie.services._base_http_service.http_services import PublicHttpService | ||||
| from mealie.services.events import create_user_event | ||||
| from mealie.services.group_services.group_mixins import create_new_group | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|  | ||||
|  | ||||
| class RegistrationService(PublicHttpService[int, str]): | ||||
|     event_func = create_user_event | ||||
|  | ||||
|     def populate_item() -> None: | ||||
|         pass | ||||
|  | ||||
|     def register_user(self, registration: CreateUserRegistration) -> PrivateUser: | ||||
|         self.registration = registration | ||||
|  | ||||
|         logger.info(f"Registering user {registration.username}") | ||||
|  | ||||
|         if registration.group: | ||||
|             group = self._create_new_group() | ||||
|         else: | ||||
|             group = self._existing_group_ref() | ||||
|  | ||||
|         return self._create_new_user(group) | ||||
|  | ||||
|     def _create_new_user(self, group: GroupInDB) -> PrivateUser: | ||||
|         new_user = UserIn( | ||||
|             email=self.registration.email, | ||||
|             username=self.registration.username, | ||||
|             password=hash_password(self.registration.password), | ||||
|             full_name=self.registration.username, | ||||
|             advanced=self.registration.advanced, | ||||
|             group=group.name, | ||||
|         ) | ||||
|  | ||||
|         return self.db.users.create(self.session, new_user) | ||||
|  | ||||
|     def _create_new_group(self) -> GroupInDB: | ||||
|         group_data = GroupBase(name=self.registration.group) | ||||
|  | ||||
|         group_preferences = CreateGroupPreferences( | ||||
|             group_id=0, | ||||
|             private_group=self.registration.private, | ||||
|             first_day_of_week=0, | ||||
|             recipe_public=not self.registration.private, | ||||
|             recipe_show_nutrition=self.registration.advanced, | ||||
|             recipe_show_assets=self.registration.advanced, | ||||
|             recipe_landscape_view=False, | ||||
|             recipe_disable_comments=self.registration.advanced, | ||||
|             recipe_disable_amount=self.registration.advanced, | ||||
|         ) | ||||
|  | ||||
|         return create_new_group(self.session, group_data, group_preferences) | ||||
|  | ||||
|     def _existing_group_ref(self) -> GroupInDB: | ||||
|         pass | ||||
| @@ -3,7 +3,7 @@ from fastapi import HTTPException, status | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.core.security import hash_password, verify_password | ||||
| from mealie.schema.user.user import ChangePassword, PrivateUser | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services._base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_user_event | ||||
| 
 | ||||
| logger = get_logger(module=__name__) | ||||
| @@ -1,43 +0,0 @@ | ||||
| import json | ||||
|  | ||||
| import pytest | ||||
| from fastapi.testclient import TestClient | ||||
|  | ||||
| from tests.app_routes import AppRoutes | ||||
|  | ||||
|  | ||||
| @pytest.fixture() | ||||
| def page_data(): | ||||
|     return {"name": "My New Page", "description": "", "position": 0, "categories": [], "groupId": 1} | ||||
|  | ||||
|  | ||||
| def test_create_cookbook(api_client: TestClient, api_routes: AppRoutes, admin_token, page_data): | ||||
|     response = api_client.post(api_routes.group_cookbook, json=page_data, headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| def test_read_cookbook(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): | ||||
|     response = api_client.get(api_routes.group_cookbook_id(1), headers=admin_token) | ||||
|  | ||||
|     page_data["id"] = 1 | ||||
|     page_data["slug"] = "my-new-page" | ||||
|  | ||||
|     assert json.loads(response.text) == page_data | ||||
|  | ||||
|  | ||||
| def test_update_cookbook(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): | ||||
|     page_data["id"] = 1 | ||||
|     page_data["name"] = "My New Name" | ||||
|     response = api_client.put(api_routes.group_cookbook_id(1), json=page_data, headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| def test_delete_cookbook(api_client: TestClient, api_routes: AppRoutes, admin_token): | ||||
|     response = api_client.delete(api_routes.group_cookbook_id(1), headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|     response = api_client.get(api_routes.group_cookbook_id(1), headers=admin_token) | ||||
|     assert response.status_code == 404 | ||||
| @@ -4,6 +4,7 @@ import pytest | ||||
| from fastapi.testclient import TestClient | ||||
|  | ||||
| from tests.app_routes import AppRoutes | ||||
| from tests.utils.assertion_helpers import assert_ignore_keys | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| @@ -41,8 +42,10 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, admin_token | ||||
|     # Validate Changes | ||||
|     response = api_client.get(api_routes.groups, headers=admin_token) | ||||
|     all_groups = json.loads(response.text) | ||||
|  | ||||
|     id_2 = filter(lambda x: x["id"] == 2, all_groups) | ||||
|     assert next(id_2) == new_data | ||||
|  | ||||
|     assert_ignore_keys(new_data, next(id_2), ["preferences"]) | ||||
|  | ||||
|  | ||||
| def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, admin_token): | ||||
|   | ||||
| @@ -13,7 +13,7 @@ def backup_data(): | ||||
|         "force": True, | ||||
|         "recipes": True, | ||||
|         "settings": False,  # ! Broken | ||||
|         "groups": True, | ||||
|         "groups": False,  # ! Also Broken | ||||
|         "users": True, | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,46 @@ | ||||
| import pytest | ||||
| from fastapi.testclient import TestClient | ||||
|  | ||||
|  | ||||
| class Routes: | ||||
|     base = "/api/groups/cookbooks" | ||||
|  | ||||
|     def item(item_id: int) -> str: | ||||
|         return f"{Routes.base}/{item_id}" | ||||
|  | ||||
|  | ||||
| @pytest.fixture() | ||||
| def page_data(): | ||||
|     return {"name": "My New Page", "description": "", "position": 0, "categories": [], "groupId": 1} | ||||
|  | ||||
|  | ||||
| def test_create_cookbook(api_client: TestClient, admin_token, page_data): | ||||
|     response = api_client.post(Routes.base, json=page_data, headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| def test_read_cookbook(api_client: TestClient, page_data, admin_token): | ||||
|     response = api_client.get(Routes.item(1), headers=admin_token) | ||||
|  | ||||
|     page_data["id"] = 1 | ||||
|     page_data["slug"] = "my-new-page" | ||||
|  | ||||
|     assert response.json() == page_data | ||||
|  | ||||
|  | ||||
| def test_update_cookbook(api_client: TestClient, page_data, admin_token): | ||||
|     page_data["id"] = 1 | ||||
|     page_data["name"] = "My New Name" | ||||
|     response = api_client.put(Routes.item(1), json=page_data, headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| def test_delete_cookbook(api_client: TestClient, admin_token): | ||||
|     response = api_client.delete(Routes.item(1), headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|     response = api_client.get(Routes.item(1), headers=admin_token) | ||||
|     assert response.status_code == 404 | ||||
| @@ -0,0 +1,32 @@ | ||||
| from fastapi.testclient import TestClient | ||||
|  | ||||
| from mealie.schema.user.registration import CreateUserRegistration | ||||
|  | ||||
|  | ||||
| class Routes: | ||||
|     base = "/api/users/register" | ||||
|     auth_token = "/api/auth/token" | ||||
|  | ||||
|  | ||||
| def test_user_registration_new_group(api_client: TestClient): | ||||
|     registration = CreateUserRegistration( | ||||
|         group="New Group Name", | ||||
|         email="email@email.com", | ||||
|         username="fake-user-name", | ||||
|         password="fake-password", | ||||
|         password_confirm="fake-password", | ||||
|         advanced=False, | ||||
|         private=False, | ||||
|     ) | ||||
|  | ||||
|     response = api_client.post(Routes.base, json=registration.dict(by_alias=True)) | ||||
|     assert response.status_code == 201 | ||||
|  | ||||
|     # Login | ||||
|     form_data = {"username": "email@email.com", "password": "fake-password"} | ||||
|  | ||||
|     response = api_client.post(Routes.auth_token, form_data) | ||||
|     assert response.status_code == 200 | ||||
|     token = response.json().get("access_token") | ||||
|  | ||||
|     assert token is not None | ||||
| @@ -0,0 +1,51 @@ | ||||
| from fastapi.testclient import TestClient | ||||
|  | ||||
| from mealie.schema.group.group_preferences import UpdateGroupPreferences | ||||
| from tests.utils.assertion_helpers import assert_ignore_keys | ||||
|  | ||||
|  | ||||
| class Routes: | ||||
|     base = "/api/groups/self" | ||||
|     preferences = "/api/groups/preferences" | ||||
|  | ||||
|  | ||||
| def test_get_preferences(api_client: TestClient, admin_token) -> None: | ||||
|     response = api_client.get(Routes.preferences, headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|     preferences = response.json() | ||||
|  | ||||
|     # Spot Check Defaults | ||||
|     assert preferences["recipePublic"] is True | ||||
|     assert preferences["recipeShowNutrition"] is False | ||||
|  | ||||
|  | ||||
| def test_preferences_in_group(api_client: TestClient, admin_token) -> None: | ||||
|     response = api_client.get(Routes.base, headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|     group = response.json() | ||||
|  | ||||
|     assert group["preferences"] is not None | ||||
|  | ||||
|     # Spot Check | ||||
|     assert group["preferences"]["recipePublic"] is True | ||||
|     assert group["preferences"]["recipeShowNutrition"] is False | ||||
|  | ||||
|  | ||||
| def test_update_preferences(api_client: TestClient, admin_token) -> None: | ||||
|     new_data = UpdateGroupPreferences(recipe_public=False, recipe_show_nutrition=True) | ||||
|  | ||||
|     response = api_client.put(Routes.preferences, json=new_data.dict(), headers=admin_token) | ||||
|  | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|     preferences = response.json() | ||||
|  | ||||
|     assert preferences is not None | ||||
|     assert preferences["recipePublic"] is False | ||||
|     assert preferences["recipeShowNutrition"] is True | ||||
|  | ||||
|     assert_ignore_keys(new_data.dict(by_alias=True), preferences, ["id", "groupId"]) | ||||
| @@ -0,0 +1,71 @@ | ||||
| import pytest | ||||
|  | ||||
| from mealie.schema.user.registration import CreateUserRegistration | ||||
|  | ||||
|  | ||||
| def test_create_user_registration() -> None: | ||||
|     CreateUserRegistration( | ||||
|         group="Home", | ||||
|         group_token=None, | ||||
|         email="SomeValidEmail@email.com", | ||||
|         username="SomeValidUsername", | ||||
|         password="SomeValidPassword", | ||||
|         password_confirm="SomeValidPassword", | ||||
|         advanced=False, | ||||
|         private=True, | ||||
|     ) | ||||
|  | ||||
|     CreateUserRegistration( | ||||
|         group=None, | ||||
|         group_token="asdfadsfasdfasdfasdf", | ||||
|         email="SomeValidEmail@email.com", | ||||
|         username="SomeValidUsername", | ||||
|         password="SomeValidPassword", | ||||
|         password_confirm="SomeValidPassword", | ||||
|         advanced=False, | ||||
|         private=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("group, group_token", [(None, None), ("", None), (None, "")]) | ||||
| def test_group_or_token_validator(group, group_token) -> None: | ||||
|     with pytest.raises(ValueError): | ||||
|         CreateUserRegistration( | ||||
|             group=group, | ||||
|             group_token=group_token, | ||||
|             email="SomeValidEmail@email.com", | ||||
|             username="SomeValidUsername", | ||||
|             password="SomeValidPassword", | ||||
|             password_confirm="SomeValidPassword", | ||||
|             advanced=False, | ||||
|             private=True, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def test_group_no_args_passed() -> None: | ||||
|     with pytest.raises(ValueError): | ||||
|         CreateUserRegistration( | ||||
|             email="SomeValidEmail@email.com", | ||||
|             username="SomeValidUsername", | ||||
|             password="SomeValidPassword", | ||||
|             password_confirm="SomeValidPassword", | ||||
|             advanced=False, | ||||
|             private=True, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def test_password_validator() -> None: | ||||
|     with pytest.raises(ValueError): | ||||
|         CreateUserRegistration( | ||||
|             group=None, | ||||
|             group_token="asdfadsfasdfasdfasdf", | ||||
|             email="SomeValidEmail@email.com", | ||||
|             username="SomeValidUsername", | ||||
|             password="SomeValidPassword", | ||||
|             password_confirm="PasswordDefNotMatch", | ||||
|             advanced=False, | ||||
|             private=True, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| test_create_user_registration() | ||||
							
								
								
									
										17
									
								
								tests/utils/assertion_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tests/utils/assertion_helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| def assert_ignore_keys(dict1: dict, dict2: dict, ignore_keys: list) -> None: | ||||
|     """ | ||||
|     Itterates through a list of keys and checks if they are in the the provided ignore_keys list, | ||||
|     if they are not in the ignore_keys list, it checks the value of the key in the provided against | ||||
|     the value provided in dict2. If the value of the key in dict1 is not equal to the value of the | ||||
|     key in dict2, The assertion fails. Useful for testing id / group_id agnostic data | ||||
|  | ||||
|     Note: ignore_keys defaults to ['id', 'group_id'] | ||||
|     """ | ||||
|     if ignore_keys is None: | ||||
|         ignore_keys = ["id", "group_id"] | ||||
|  | ||||
|     for key, value in dict1.items(): | ||||
|         if key in ignore_keys: | ||||
|             continue | ||||
|         else: | ||||
|             assert value == dict2[key] | ||||
		Reference in New Issue
	
	Block a user