profile image upload

This commit is contained in:
hay-kot 2021-02-25 19:10:14 -09:00
commit 1f16d0cb39
9 changed files with 127 additions and 24 deletions

View file

@ -12,7 +12,6 @@ const categoryURLs = {
export default { export default {
async get_all() { async get_all() {
let response = await apiReq.get(categoryURLs.get_all); let response = await apiReq.get(categoryURLs.get_all);
console.log("All Cats", response.data);
return response.data; return response.data;
}, },
async get_recipes_in_category(category) { async get_recipes_in_category(category) {

View file

@ -117,7 +117,6 @@ export default {
}, },
async mounted() { async mounted() {
let settings = await api.settings.requestAll(); let settings = await api.settings.requestAll();
console.log("Settings", settings.planCategories);
this.items = await api.recipes.getAllByCategory(settings.planCategories); this.items = await api.recipes.getAllByCategory(settings.planCategories);
}, },

View file

@ -15,6 +15,7 @@ export default {
url: String, url: String,
text: { default: "Upload" }, text: { default: "Upload" },
icon: { default: "mdi-cloud-upload" }, icon: { default: "mdi-cloud-upload" },
fileName: { defaul: "archive" },
}, },
data: () => ({ data: () => ({
file: null, file: null,
@ -32,7 +33,7 @@ export default {
if (this.file != null) { if (this.file != null) {
this.isSelecting = true; this.isSelecting = true;
let formData = new FormData(); let formData = new FormData();
formData.append("archive", this.file); formData.append(this.fileName, this.file);
await api.utils.uploadFile(this.url, formData); await api.utils.uploadFile(this.url, formData);

View file

@ -0,0 +1,17 @@
export const initials = {
computed: {
initials() {
const allNames = this.user.fullName.trim().split(" ");
const initials = allNames.reduce(
(acc, curr, index) => {
if (index === 0 || index === allNames.length - 1) {
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
}
return acc;
},
[""]
);
return initials;
},
},
};

View file

@ -1,6 +1,5 @@
import { store } from "@/store"; import { store } from "@/store";
export const user = { export const user = {
data() {},
computed: { computed: {
user() { user() {
return store.getters.getUserData; return store.getters.getUserData;
@ -8,5 +7,18 @@ export const user = {
loggedIn() { loggedIn() {
return store.getters.getIsLoggedIn; return store.getters.getIsLoggedIn;
}, },
initials() {
const allNames = this.user.fullName.trim().split(" ");
const initials = allNames.reduce(
(acc, curr, index) => {
if (index === 0 || index === allNames.length - 1) {
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
}
return acc;
},
[""]
);
return initials;
},
}, },
}; };

View file

@ -33,18 +33,22 @@
</v-app-bar> </v-app-bar>
<v-card-text> <v-card-text>
<v-container> <v-form ref="newUser">
<v-row> <v-row>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<v-text-field <v-text-field
v-model="editedItem.fullName" v-model="editedItem.fullName"
label="Full Name" label="Full Name"
:rules="[existsRule]"
validate-on-blur
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<v-text-field <v-text-field
v-model="editedItem.email" v-model="editedItem.email"
label="Email" label="Email"
:rules="[existsRule, emailRule]"
validate-on-blur
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
@ -57,6 +61,7 @@
<v-text-field <v-text-field
v-model="editedItem.password" v-model="editedItem.password"
label="User Password" label="User Password"
:rules="[existsRule, minRule]"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="12" md="3"> <v-col cols="12" sm="12" md="3">
@ -66,7 +71,7 @@
></v-switch> ></v-switch>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
@ -121,8 +126,10 @@
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import Confirmation from "@/components/UI/Confirmation";
import api from "@/api"; import api from "@/api";
import { validators } from "@/mixins/validators";
export default { export default {
components: { Confirmation }, components: { Confirmation },
mixins: [validators],
data: () => ({ data: () => ({
dialog: false, dialog: false,
activeId: null, activeId: null,
@ -145,6 +152,7 @@ export default {
editedItem: { editedItem: {
id: 0, id: 0,
fullName: "", fullName: "",
password: "",
email: "", email: "",
family: "", family: "",
admin: false, admin: false,
@ -152,6 +160,7 @@ export default {
defaultItem: { defaultItem: {
id: 0, id: 0,
fullName: "", fullName: "",
password: "",
email: "", email: "",
family: "", family: "",
admin: false, admin: false,
@ -228,11 +237,12 @@ export default {
async save() { async save() {
if (this.editedIndex > -1) { if (this.editedIndex > -1) {
api.users.update(this.editedItem); api.users.update(this.editedItem);
} else { this.close();
} else if (this.$refs.newUser.validate()) {
api.users.create(this.editedItem); api.users.create(this.editedItem);
this.close();
} }
await this.initialize(); await this.initialize();
this.close();
}, },
}, },
}; };

View file

@ -21,11 +21,20 @@
<v-card-text> <v-card-text>
<v-row> <v-row>
<v-col cols="12" md="3" align="center" justify="center"> <v-col cols="12" md="3" align="center" justify="center">
<v-avatar color="accent" size="120" class="mr-2" v-if="!loading"> <v-avatar
color="accent"
size="120"
v-if="!loading"
class="white--text headline mr-2"
>
<img <img
src="https://cdn.vuetifyjs.com/images/john.jpg" :src="userProfileImage"
alt="John" v-if="!hideImage"
@error="hideImage = true"
/> />
<div v-else>
{{ initials }}
</div>
</v-avatar> </v-avatar>
</v-col> </v-col>
<v-col cols="12" md="9"> <v-col cols="12" md="9">
@ -60,7 +69,12 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<UploadBtn icon="mdi-image-area" text="Upload Photo" /> <UploadBtn
icon="mdi-image-area"
text="Upload Photo"
:url="userProfileImage"
file-name="profile_image"
/>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="success" class="mr-2" @click="updateUser"> <v-btn color="success" class="mr-2" @click="updateUser">
@ -110,17 +124,16 @@
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-btn icon @click="showPassword = !showPassword" :loading="passwordLoading"> <v-btn
icon
@click="showPassword = !showPassword"
:loading="passwordLoading"
>
<v-icon v-if="!showPassword">mdi-eye-off</v-icon> <v-icon v-if="!showPassword">mdi-eye-off</v-icon>
<v-icon v-else> mdi-eye </v-icon> <v-icon v-else> mdi-eye </v-icon>
</v-btn> </v-btn>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn color="accent" class="mr-2" @click="changePassword">
color="accent"
class="mr-2"
@click="changePassword"
>
<v-icon left> mdi-lock </v-icon> <v-icon left> mdi-lock </v-icon>
{{ $t("settings.change-password") }} {{ $t("settings.change-password") }}
</v-btn> </v-btn>
@ -135,13 +148,15 @@
import UploadBtn from "@/components/UI/UploadBtn"; import UploadBtn from "@/components/UI/UploadBtn";
import api from "@/api"; import api from "@/api";
import { validators } from "@/mixins/validators"; import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
export default { export default {
components: { components: {
UploadBtn, UploadBtn,
}, },
mixins: [validators], mixins: [validators, initials],
data() { data() {
return { return {
hideImage: false,
passwordLoading: false, passwordLoading: false,
password: { password: {
current: "", current: "",
@ -160,6 +175,12 @@ export default {
}; };
}, },
computed: {
userProfileImage() {
return `api/users/${this.user.id}/image`;
},
},
async mounted() { async mounted() {
this.refreshProfile(); this.refreshProfile();
}, },

View file

@ -12,7 +12,7 @@ def ensure_dirs():
# Register ENV # Register ENV
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me! ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
dotenv.load_dotenv(ENV) dotenv.load_dotenv(ENV)
@ -45,6 +45,7 @@ MIGRATION_DIR = DATA_DIR.joinpath("migration")
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud") NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown") CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
TEMPLATE_DIR = DATA_DIR.joinpath("templates") TEMPLATE_DIR = DATA_DIR.joinpath("templates")
USER_DIR = DATA_DIR.joinpath("users")
SQLITE_DIR = DATA_DIR.joinpath("db") SQLITE_DIR = DATA_DIR.joinpath("db")
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes") RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
TEMP_DIR = DATA_DIR.joinpath(".temp") TEMP_DIR = DATA_DIR.joinpath(".temp")
@ -59,7 +60,8 @@ REQUIRED_DIRS = [
SQLITE_DIR, SQLITE_DIR,
NEXTCLOUD_DIR, NEXTCLOUD_DIR,
CHOWDOWN_DIR, CHOWDOWN_DIR,
RECIPE_DATA_DIR RECIPE_DATA_DIR,
USER_DIR,
] ]
ensure_dirs() ensure_dirs()

View file

@ -1,10 +1,13 @@
import shutil
from datetime import timedelta from datetime import timedelta
from core.config import USER_DIR
from core.security import get_password_hash, verify_password from core.security import get_password_hash, verify_password
from db.database import db from db.database import db
from db.db_setup import generate_session from db.db_setup import generate_session
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, File, UploadFile
from routes.deps import manager, query_user from fastapi.responses import FileResponse
from routes.deps import manager
from schema.snackbar import SnackResponse from schema.snackbar import SnackResponse
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -74,6 +77,45 @@ async def update_user(
return SnackResponse.success("User Updated", access_token) return SnackResponse.success("User Updated", access_token)
@router.get("/{id}/image")
async def get_user_image(id: str):
""" Returns a users profile picture """
user_dir = USER_DIR.joinpath(id)
for recipe_image in user_dir.glob("profile_image.*"):
print(recipe_image)
return FileResponse(recipe_image)
else:
return False
@router.post("/{id}/image")
async def update_user_image(
id: str,
profile_image: UploadFile = File(...),
current_user: UserInDB = Depends(manager),
):
""" Updates a User Image """
extension = profile_image.filename.split(".")[-1]
USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
try:
[x.unlink() for x in USER_DIR.join(id).glob("profile_image.*")]
except:
pass
dest = USER_DIR.joinpath(id, f"profile_image.{extension}")
with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer)
if dest.is_file:
return SnackResponse.success("Backup uploaded")
else:
return SnackResponse.error("Failure uploading file")
@router.put("/{id}/password") @router.put("/{id}/password")
async def update_password( async def update_password(
id: int, id: int,