feat: csv user import frontend

This commit is contained in:
Timothy J. Baek 2024-05-01 19:02:25 -07:00
parent e5703a588f
commit bd3b5f1edb
4 changed files with 188 additions and 54 deletions

View file

@ -1,12 +1,14 @@
import logging import logging
from fastapi import Request from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import re import re
import uuid import uuid
import csv
from apps.web.models.auths import ( from apps.web.models.auths import (
SigninForm, SigninForm,
@ -212,7 +214,7 @@ async def signup(request: Request, form_data: SignupForm):
@router.post("/add", response_model=SigninResponse) @router.post("/add", response_model=SigninResponse)
async def signup(form_data: AddUserForm, user=Depends(get_admin_user)): async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
if not validate_email_format(form_data.email.lower()): if not validate_email_format(form_data.email.lower()):
raise HTTPException( raise HTTPException(
@ -251,6 +253,76 @@ async def signup(form_data: AddUserForm, user=Depends(get_admin_user)):
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
@router.post("/add/import", response_model=SigninResponse)
async def add_user_csv_import(
file: UploadFile = File(...), user=Depends(get_admin_user)
):
try:
# Check if the file is a CSV file
if file.filename.endswith(".csv"):
# Read the contents of the CSV file
contents = await file.read()
# Decode the contents from bytes to string
decoded_content = contents.decode("utf-8")
# Split the CSV content into lines
csv_lines = decoded_content.split("\n")
# Parse CSV
csv_data = []
csv_reader = csv.reader(csv_lines)
for row in csv_reader:
csv_data.append(row)
# Print the CSV data
for row in csv_data:
print(row)
return {"message": "CSV file uploaded successfully."}
else:
raise "File must be a CSV."
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
# if not validate_email_format(form_data.email.lower()):
# raise HTTPException(
# status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
# )
# if Users.get_user_by_email(form_data.email.lower()):
# raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
# try:
# print(form_data)
# hashed = get_password_hash(form_data.password)
# user = Auths.insert_new_auth(
# form_data.email.lower(),
# hashed,
# form_data.name,
# form_data.profile_image_url,
# form_data.role,
# )
# if user:
# token = create_token(data={"id": user.id})
# return {
# "token": token,
# "token_type": "Bearer",
# "id": user.id,
# "email": user.email,
# "name": user.name,
# "role": user.role,
# "profile_image_url": user.profile_image_url,
# }
# else:
# raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
# except Exception as err:
# raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
############################ ############################
# ToggleSignUp # ToggleSignUp
############################ ############################

View file

@ -0,0 +1 @@
Name,Email,Password,Role
1 Name Email Password Role

View file

@ -5,12 +5,16 @@
import { addUser } from '$lib/apis/auths'; import { addUser } from '$lib/apis/auths';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let show = false; export let show = false;
let tab = '';
let inputFiles;
let _user = { let _user = {
name: '', name: '',
email: '', email: '',
@ -76,69 +80,126 @@
submitHandler(); submitHandler();
}} }}
> >
<div class=" "> <div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
<div class="flex flex-col w-full"> <button
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div> class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = '';
}}>Form</button
>
<div class="flex-1"> <button
<select class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" type="button"
bind:value={_user.role} on:click={() => {
placeholder={$i18n.t('Enter Your Role')} tab = 'import';
required }}>CSV Import</button
> >
<option value="pending"> pending </option> </div>
<option value="user"> user </option> <div class="px-1">
<option value="admin"> admin </option> {#if tab === ''}
</select> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
<div class="flex-1">
<select
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
bind:value={_user.role}
placeholder={$i18n.t('Enter Your Role')}
required
>
<option value="pending"> pending </option>
<option value="user"> user </option>
<option value="admin"> admin </option>
</select>
</div>
</div> </div>
</div>
<div class="flex flex-col w-full mt-2"> <div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="text" type="text"
bind:value={_user.name} bind:value={_user.name}
placeholder={$i18n.t('Enter Your Full Name')} placeholder={$i18n.t('Enter Your Full Name')}
autocomplete="off" autocomplete="off"
required required
/> />
</div>
</div> </div>
</div>
<hr class=" dark:border-gray-800 my-3 w-full" /> <hr class=" dark:border-gray-800 my-3 w-full" />
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="email" type="email"
bind:value={_user.email} bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')} placeholder={$i18n.t('Enter Your Email')}
autocomplete="off" autocomplete="off"
required required
/> />
</div>
</div> </div>
</div>
<div class="flex flex-col w-full mt-2"> <div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="password" type="password"
bind:value={_user.password} bind:value={_user.password}
placeholder={$i18n.t('Enter Your Password')} placeholder={$i18n.t('Enter Your Password')}
autocomplete="off" autocomplete="off"
/> />
</div>
</div> </div>
</div> {:else if tab === 'import'}
<div>
<div class="mb-3 w-full">
<input
id="upload-user-csv-input"
hidden
bind:files={inputFiles}
type="file"
accept=".csv"
/>
<button
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
type="button"
on:click={() => {
document.getElementById('upload-user-csv-input')?.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
{$i18n.t('Click here to select a csv file.')}
{/if}
</button>
</div>
<div class=" text-xs text-gray-500">
{$i18n.t(
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
)}
<a
class="underline dark:text-gray-200"
href="{WEBUI_BASE_URL}/static/user-import.csv"
>
Click here to download user import template file.
</a>
</div>
</div>
{/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">

View file

@ -154,7 +154,7 @@
if (isCtrlPressed && event.key === '.') { if (isCtrlPressed && event.key === '.') {
event.preventDefault(); event.preventDefault();
console.log('openSettings'); console.log('openSettings');
document.getElementById('open-settings-button')?.click(); showSettings.set(!$showSettings);
} }
// Check if Ctrl + / is pressed // Check if Ctrl + / is pressed