import groups and users

This commit is contained in:
hay-kot 2021-03-20 19:28:04 -08:00
commit f7732d4b83
12 changed files with 326 additions and 171 deletions

View file

@ -25,24 +25,7 @@
<v-card-text>
<v-row>
<v-col>
<v-checkbox
class="mb-n4 mt-1"
dense
:label="$t('settings.backup.import-recipes')"
v-model="importRecipes"
></v-checkbox>
<v-checkbox
class="my-n4"
dense
:label="$t('settings.backup.import-themes')"
v-model="importThemes"
></v-checkbox>
<v-checkbox
class="my-n4"
dense
:label="$t('settings.backup.import-settings')"
v-model="importSettings"
></v-checkbox>
<ImportOptions @update-options="updateOptions" class="mt-5" />
</v-col>
<!-- <v-col>
<v-tooltip top>
@ -104,7 +87,9 @@
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
export default {
components: { ImportOptions },
props: {
name: {
default: "Backup Name",
@ -115,15 +100,22 @@ export default {
},
data() {
return {
options: {
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
dialog: false,
importRecipes: true,
forceImport: false,
rebaseImport: false,
importThemes: false,
importSettings: false,
};
},
methods: {
updateOptions(options) {
this.options = options;
},
open() {
this.dialog = true;
},
@ -133,11 +125,13 @@ export default {
raiseEvent(event) {
let eventData = {
name: this.name,
recipes: this.importRecipes,
force: this.forceImport,
rebase: this.rebaseImport,
themes: this.importThemes,
settings: this.importSettings,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
};
this.close();
this.$emit(event, eventData);

View file

@ -0,0 +1,62 @@
<template>
<div>
<v-checkbox
v-for="option in options"
:key="option.text"
class="mb-n4 mt-n3"
dense
:label="option.text"
v-model="option.value"
@change="emitValue()"
></v-checkbox>
</div>
</template>
<script>
const UPDATE_EVENT = "update-options";
export default {
data() {
return {
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
settings: {
value: true,
text: this.$t("general.settings"),
},
themes: {
value: true,
text: this.$t("general.themes"),
},
users: {
value: true,
text: this.$t("general.users"),
},
groups: {
value: true,
text: this.$t("general.groups"),
},
},
};
},
mounted() {
this.emitValue();
},
methods: {
emitValue() {
this.$emit(UPDATE_EVENT, {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
users: this.options.users.value,
groups: this.options.groups.value,
});
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -58,28 +58,40 @@
<v-tab>Recipes</v-tab>
<v-tab>Themes</v-tab>
<v-tab>Settings</v-tab>
<v-tab>Users</v-tab>
<v-tab>Groups</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item>
<v-card flat>
<DataTable :data-headers="recipeHeaders" :data-set="recipeData" />
<DataTable :data-headers="importHeaders" :data-set="recipeData" />
</v-card>
</v-tab-item>
<v-tab-item>
<v-card>
<DataTable
:data-headers="recipeHeaders"
:data-headers="importHeaders"
:data-set="themeData"
/> </v-card
></v-tab-item>
<v-tab-item>
<v-card
><DataTable
:data-headers="recipeHeaders"
:data-headers="importHeaders"
:data-set="settingsData"
/>
</v-card>
</v-tab-item>
<v-tab-item>
<v-card
><DataTable :data-headers="importHeaders" :data-set="userData" />
</v-card>
</v-tab-item>
<v-tab-item>
<v-card
><DataTable :data-headers="importHeaders" :data-set="groupData" />
</v-card>
</v-tab-item>
</v-tabs-items>
</v-card>
</v-dialog>
@ -98,7 +110,9 @@ export default {
recipeData: [],
themeData: [],
settingsData: [],
recipeHeaders: [
userData: [],
groupData: [],
importHeaders: [
{
text: "Status",
value: "status",
@ -117,39 +131,40 @@ export default {
computed: {
recipeNumbers() {
let numbers = { success: 0, failure: 0 };
this.recipeData.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
return this.calculateNumbers(this.recipeData);
},
settingsNumbers() {
let numbers = { success: 0, failure: 0 };
this.settingsData.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
return this.calculateNumbers(this.settingsData);
},
themeNumbers() {
let numbers = { success: 0, failure: 0 };
this.themeData.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
return this.calculateNumbers(this.themeData);
},
userNumbers() {
return this.calculateNumbers(this.userData);
},
groupNumbers() {
return this.calculateNumbers(this.groupData);
},
},
methods: {
calculateNumbers(list_array) {
if (!list_array) return;
let numbers = { success: 0, failure: 0 };
list_array.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
},
open(importData) {
console.log(importData);
this.recipeData = importData.recipeImports;
this.themeData = importData.themeReport;
this.settingsData = importData.settingsReport;
this.themeData = importData.themeImports;
this.settingsData = importData.settingsImports;
this.userData = importData.userImports;
this.groupData = importData.groupImports;
this.dialog = true;
},
},

View file

@ -15,19 +15,13 @@
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
<v-card-text v-if="!fullBackup" class="mt-n6">
<v-expand-transition>
<div v-if="!fullBackup">
<v-card-text class="mt-n4">
<v-row>
<v-col sm="4">
<p>{{ $t("general.options") }}:</p>
<v-checkbox
v-for="option in options"
:key="option.text"
class="mb-n4 mt-n3"
dense
:label="option.text"
v-model="option.value"
></v-checkbox>
<ImportOptions @update-options="updateOptions" class="mt-5" />
</v-col>
<v-col>
<p>{{ $t("general.templates") }}:</p>
@ -42,30 +36,27 @@
</v-col>
</v-row>
</v-card-text>
</div>
</v-expand-transition>
</v-card>
</template>
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
import api from "@/api";
export default {
components: { ImportOptions },
data() {
return {
tag: null,
fullBackup: true,
loading: false,
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
settings: {
value: true,
text: this.$t("general.settings"),
},
themes: {
value: true,
text: this.$t("general.themes"),
},
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
availableTemplates: [],
selectedTemplates: [],
@ -82,6 +73,9 @@ export default {
},
},
methods: {
updateOptions(options) {
this.options = options;
},
async getAvailableBackups() {
let response = await api.backups.requestAvailable();
response.templates.forEach(element => {
@ -94,9 +88,11 @@ export default {
let data = {
tag: this.tag,
options: {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
},
templates: this.selectedTemplates,
};

View file

@ -46,7 +46,9 @@
"token": "Token",
"field-required": "Field Required",
"apply": "Apply",
"current-parenthesis": "(Current)"
"current-parenthesis": "(Current)",
"users": "Users",
"groups": "Groups"
},
"page": {
"home-page": "Home Page",

View file

@ -17,7 +17,7 @@ function inDarkMode(payload) {
const state = {
activeTheme: {},
darkMode: "system",
darkMode: "light",
isDark: false,
isLoggedIn: false,
token: "",

View file

@ -18,7 +18,6 @@ app = FastAPI(
redoc_url=redoc_url,
)
def start_scheduler():
import mealie.services.scheduler.scheduled_jobs
@ -56,6 +55,7 @@ def main():
host="0.0.0.0",
port=PORT,
reload=True,
reload_dirs=["mealie"],
debug=True,
log_level="info",
workers=1,

View file

@ -28,6 +28,7 @@ class User(SqlAlchemyBase, BaseMixins):
password,
group: str = DEFAULT_GROUP,
admin=False,
id=None,
) -> None:
group = group if group else DEFAULT_GROUP
@ -37,11 +38,14 @@ class User(SqlAlchemyBase, BaseMixins):
self.admin = admin
self.password = password
def update(self, full_name, email, group, admin, session=None):
def update(self, full_name, email, group, admin, session=None, id=None, password=None):
self.full_name = full_name
self.email = email
self.group = Group.create_if_not_exist(session, group)
self.admin = admin
if password:
self.password = password
def update_password(self, password):
self.password = password

View file

@ -1,13 +1,13 @@
import operator
import shutil
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.schema.snackbar import SnackResponse
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.backups.imports import ImportDatabase
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
@ -41,6 +41,8 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
export_recipes=data.options.recipes,
export_settings=data.options.settings,
export_themes=data.options.themes,
export_users=data.options.users,
export_groups=data.options.groups,
)
try:
return SnackResponse.success("Backup Created at " + export_path)
@ -80,17 +82,18 @@ async def upload_nextcloud_zipfile(file_name: str):
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
""" Import a database backup file generated from Mealie. """
import_db = ImportDatabase(
imported = imports.import_database(
session=session,
zip_archive=import_data.name,
archive=import_data.name,
import_recipes=import_data.recipes,
force_import=import_data.force,
rebase=import_data.rebase,
import_settings=import_data.settings,
import_themes=import_data.themes,
import_users=import_data.users,
import_groups=import_data.groups,
force_import=import_data.force,
rebase=import_data.rebase,
)
imported = import_db.run()
return imported

View file

@ -23,6 +23,24 @@ class BackupOptions(BaseModel):
}
class ImportJob(BackupOptions):
name: str
force: bool = False
rebase: bool = False
class Config:
schema_extra = {
"example": {
"name": "my_local_backup.zip",
"recipes": True,
"force": False,
"rebase": False,
"themes": False,
"settings": False,
}
}
class BackupJob(BaseModel):
tag: Optional[str]
options: BackupOptions
@ -59,24 +77,3 @@ class Imports(BaseModel):
"templates": ["recipes.md", "custom_template.md"],
}
}
class ImportJob(BaseModel):
name: str
recipes: bool
force: bool = False
rebase: bool = False
themes: bool = False
settings: bool = False
class Config:
schema_extra = {
"example": {
"name": "my_local_backup.zip",
"recipes": True,
"force": False,
"rebase": False,
"themes": False,
"settings": False,
}
}

View file

@ -3,20 +3,27 @@ from typing import Optional
from pydantic.main import BaseModel
class RecipeImport(BaseModel):
name: Optional[str]
class ImportBase(BaseModel):
name: str
status: bool
exception: Optional[str]
class RecipeImport(ImportBase):
slug: str
status: bool
exception: Optional[str]
class ThemeImport(BaseModel):
name: str
status: bool
exception: Optional[str]
class ThemeImport(ImportBase):
pass
class SettingsImport(BaseModel):
name: str
status: bool
exception: Optional[str]
class SettingsImport(ImportBase):
pass
class GroupImport(ImportBase):
pass
class UserImport(ImportBase):
pass

View file

@ -4,13 +4,14 @@ import zipfile
from pathlib import Path
from typing import List
from fastapi.logger import logger
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi.logger import logger
from mealie.schema.recipe import Recipe
from mealie.schema.restore import RecipeImport, SettingsImport, ThemeImport
from mealie.schema.restore import GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
from mealie.schema.theme import SiteTheme
from mealie.schema.user import UpdateGroup, UserInDB
from sqlalchemy.orm.session import Session
@ -19,32 +20,21 @@ class ImportDatabase:
self,
session: Session,
zip_archive: str,
import_recipes: bool = True,
import_settings: bool = True,
import_themes: bool = True,
force_import: bool = False,
rebase: bool = False,
) -> None:
"""Import a database.zip file exported from mealie.
Args:
session (Session): SqlAlchemy Session
zip_archive (str): The filename contained in the backups directory
import_recipes (bool, optional): Import Recipes?. Defaults to True.
import_settings (bool, optional): Determines if settings are imported. Defaults to True.
import_themes (bool, optional): Determines if themes are imported. Defaults to True.
force_import (bool, optional): Force import will update all existing recipes. If False existing recipes are skipped. Defaults to False.
rebase (bool, optional): Rebase will first clear the database and then import Recipes. Defaults to False.
Raises:
Exception: If the zip file does not exists an exception raise.
"""
self.session = session
self.archive = BACKUP_DIR.joinpath(zip_archive)
self.imp_recipes = import_recipes
self.imp_settings = import_settings
self.imp_themes = import_themes
self.force_imports = force_import
self.force_rebase = rebase
if self.archive.is_file():
self.import_dir = TEMP_DIR.joinpath("active_import")
@ -56,25 +46,6 @@ class ImportDatabase:
else:
raise Exception("Import file does not exist")
def run(self):
recipe_report = []
settings_report = []
theme_report = []
if self.imp_recipes:
recipe_report = self.import_recipes()
if self.imp_settings:
settings_report = self.import_settings()
if self.imp_themes:
theme_report = self.import_themes()
self.clean_up()
return {
"recipeImports": recipe_report,
"settingsReport": settings_report,
"themeReport": theme_report,
}
def import_recipes(self):
session = create_session()
recipe_dir: Path = self.import_dir.joinpath("recipes")
@ -180,8 +151,112 @@ class ImportDatabase:
import_status = SettingsImport(name=name, status=False, exception=str(inst))
settings_imports.append(import_status)
return settings_imports
def import_groups(self):
groups_file = self.import_dir.joinpath("groups", "groups.json")
group_imports = []
with open(groups_file, "r") as f:
groups = [UpdateGroup(**g) for g in json.loads(f.read())]
for group in groups:
item = db.groups.get(self.session, group.name, "name")
if item:
import_status = GroupImport(name=group.name, status=False, exception="Group Exists")
group_imports.append(import_status)
continue
try:
db.groups.create(self.session, group.dict())
import_status = GroupImport(name=group.name, status=True)
except Exception as inst:
import_status = GroupImport(name=group.name, status=False, exception=str(inst))
group_imports.append(import_status)
print(group_imports)
return group_imports
def import_users(self):
users_file = self.import_dir.joinpath("users", "users.json")
user_imports = []
with open(users_file, "r") as f:
users = [UserInDB(**g) for g in json.loads(f.read())]
for user in users:
if user.id == 1:
db.users.update(self.session, 1, user.dict())
import_status = UserImport(name=user.full_name, status=True)
user_imports.append(import_status)
continue
item = db.users.get(self.session, user.email, "email")
if item:
import_status = UserImport(name=user.full_name, status=False, exception="User Email Exists")
user_imports.append(import_status)
continue
try:
db.users.create(self.session, user.dict())
import_status = UserImport(name=user.full_name, status=True)
except Exception as inst:
import_status = UserImport(name=user.full_name, status=False, exception=str(inst))
user_imports.append(import_status)
return user_imports
def clean_up(self):
shutil.rmtree(TEMP_DIR)
def import_database(
session: Session,
archive,
import_recipes=True,
import_settings=True,
import_themes=True,
import_users=True,
import_groups=True,
force_import: bool = False,
rebase: bool = False,
):
import_session = ImportDatabase(session, archive)
recipe_report = []
if import_recipes:
recipe_report = import_session.import_recipes()
settings_report = []
if import_settings:
settings_report = import_session.import_settings()
theme_report = []
if import_themes:
theme_report = import_session.import_themes()
group_report = []
if import_groups:
print("Import Groups")
group_report = import_session.import_groups()
user_report = []
if import_users:
user_report = import_session.import_users()
import_session.clean_up()
data = {
"recipeImports": recipe_report,
"settingsImports": settings_report,
"themeImports": theme_report,
"groupImports": group_report,
"userImports": user_report,
}
print(data)
return data