Merge pull request #140 from learnhouse/swve/eng-141-january-bug-fixes

January Bug Fixes & Improvements
This commit is contained in:
Badr B 2024-02-05 22:48:15 +01:00 committed by GitHub
commit f736f41f76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 3347 additions and 1569 deletions

View file

@ -9,16 +9,22 @@ on:
jobs: jobs:
next-lint: next-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18]
steps: steps:
- name: Checkout code - uses: actions/checkout@v3
uses: actions/checkout@v2 - uses: pnpm/action-setup@v2
- name: Use Node.js
uses: actions/setup-node@v2
with: with:
node-version: 18.x version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install
working-directory: ./apps/web working-directory: ./apps/web
- name: Lint code - name: Lint
run: npm run lint run: pnpm run lint
working-directory: ./apps/web working-directory: ./apps/web

2
apps/api/.gitignore vendored
View file

@ -10,7 +10,7 @@ __pycache__/
.vscode/ .vscode/
# Learnhouse # Learnhouse
content/org_* content/*
# Flyio # Flyio
fly.toml fly.toml

848
apps/api/poetry.lock generated
View file

@ -1205,23 +1205,6 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
] ]
[[package]]
name = "jinja2"
version = "3.1.3"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "jmespath" name = "jmespath"
version = "1.0.1" version = "1.0.1"
@ -1233,17 +1216,6 @@ files = [
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
] ]
[[package]]
name = "joblib"
version = "1.3.2"
description = "Lightweight pipelining with Python functions"
optional = false
python-versions = ">=3.7"
files = [
{file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"},
{file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"},
]
[[package]] [[package]]
name = "jsonpatch" name = "jsonpatch"
version = "1.33" version = "1.33"
@ -1416,75 +1388,6 @@ files = [
pydantic = ">=1,<3" pydantic = ">=1,<3"
requests = ">=2,<3" requests = ">=2,<3"
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]] [[package]]
name = "marshmallow" name = "marshmallow"
version = "3.20.2" version = "3.20.2"
@ -1742,49 +1645,6 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
] ]
[[package]]
name = "networkx"
version = "3.2.1"
description = "Python package for creating and manipulating graphs and networks"
optional = false
python-versions = ">=3.9"
files = [
{file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"},
{file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"},
]
[package.extras]
default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"]
developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"]
doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"]
extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"]
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
[[package]]
name = "nltk"
version = "3.8.1"
description = "Natural Language Toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "nltk-3.8.1-py3-none-any.whl", hash = "sha256:fd5c9109f976fa86bcadba8f91e47f5e9293bd034474752e92a520f81c93dda5"},
{file = "nltk-3.8.1.zip", hash = "sha256:1834da3d0682cba4f2cede2f9aad6b0fafb6461ba451db0efb6f9c39798d64d3"},
]
[package.dependencies]
click = "*"
joblib = "*"
regex = ">=2021.8.3"
tqdm = "*"
[package.extras]
all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"]
corenlp = ["requests"]
machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"]
plot = ["matplotlib"]
tgrep = ["pyparsing"]
twitter = ["twython"]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "1.26.3" version = "1.26.3"
@ -1830,147 +1690,6 @@ files = [
{file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"},
] ]
[[package]]
name = "nvidia-cublas-cu12"
version = "12.1.3.1"
description = "CUBLAS native runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"},
{file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"},
]
[[package]]
name = "nvidia-cuda-cupti-cu12"
version = "12.1.105"
description = "CUDA profiling tools runtime libs."
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"},
{file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"},
]
[[package]]
name = "nvidia-cuda-nvrtc-cu12"
version = "12.1.105"
description = "NVRTC native runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"},
{file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"},
]
[[package]]
name = "nvidia-cuda-runtime-cu12"
version = "12.1.105"
description = "CUDA Runtime native Libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"},
{file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"},
]
[[package]]
name = "nvidia-cudnn-cu12"
version = "8.9.2.26"
description = "cuDNN runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"},
]
[package.dependencies]
nvidia-cublas-cu12 = "*"
[[package]]
name = "nvidia-cufft-cu12"
version = "11.0.2.54"
description = "CUFFT native runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"},
{file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"},
]
[[package]]
name = "nvidia-curand-cu12"
version = "10.3.2.106"
description = "CURAND native runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"},
{file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"},
]
[[package]]
name = "nvidia-cusolver-cu12"
version = "11.4.5.107"
description = "CUDA solver native runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"},
{file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"},
]
[package.dependencies]
nvidia-cublas-cu12 = "*"
nvidia-cusparse-cu12 = "*"
nvidia-nvjitlink-cu12 = "*"
[[package]]
name = "nvidia-cusparse-cu12"
version = "12.1.0.106"
description = "CUSPARSE native runtime libraries"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"},
{file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"},
]
[package.dependencies]
nvidia-nvjitlink-cu12 = "*"
[[package]]
name = "nvidia-nccl-cu12"
version = "2.18.1"
description = "NVIDIA Collective Communication Library (NCCL) Runtime"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_nccl_cu12-2.18.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:1a6c4acefcbebfa6de320f412bf7866de856e786e0462326ba1bac40de0b5e71"},
]
[[package]]
name = "nvidia-nvjitlink-cu12"
version = "12.3.101"
description = "Nvidia JIT LTO Library"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_nvjitlink_cu12-12.3.101-py3-none-manylinux1_x86_64.whl", hash = "sha256:64335a8088e2b9d196ae8665430bc6a2b7e6ef2eb877a9c735c804bd4ff6467c"},
{file = "nvidia_nvjitlink_cu12-12.3.101-py3-none-win_amd64.whl", hash = "sha256:1b2e317e437433753530792f13eece58f0aec21a2b05903be7bffe58a606cbd1"},
]
[[package]]
name = "nvidia-nvtx-cu12"
version = "12.1.105"
description = "NVIDIA Tools Extension"
optional = false
python-versions = ">=3"
files = [
{file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"},
{file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"},
]
[[package]] [[package]]
name = "oauthlib" name = "oauthlib"
version = "3.2.2" version = "3.2.2"
@ -2256,91 +1975,6 @@ bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"] totp = ["cryptography"]
[[package]]
name = "pillow"
version = "10.2.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.3.0" version = "1.3.0"
@ -2796,6 +2430,7 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -3020,290 +2655,6 @@ botocore = ">=1.33.2,<2.0a.0"
[package.extras] [package.extras]
crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
[[package]]
name = "safetensors"
version = "0.4.1"
description = ""
optional = false
python-versions = ">=3.7"
files = [
{file = "safetensors-0.4.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:cba01c6b76e01ec453933b3b3c0157c59b52881c83eaa0f7666244e71aa75fd1"},
{file = "safetensors-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a8f6f679d97ea0135c7935c202feefbd042c149aa70ee759855e890c01c7814"},
{file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc2ce1f5ae5143a7fb72b71fa71db6a42b4f6cf912aa3acdc6b914084778e68"},
{file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d87d993eaefe6611a9c241a8bd364a5f1ffed5771c74840363a6c4ed8d868f6"},
{file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:097e9af2efa8778cd2f0cba451784253e62fa7cc9fc73c0744d27212f7294e25"},
{file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d10a9f7bae608ccfdc009351f01dc3d8535ff57f9488a58a4c38e45bf954fe93"},
{file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270b99885ec14abfd56c1d7f28ada81740a9220b4bae960c3de1c6fe84af9e4d"},
{file = "safetensors-0.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:285b52a481e7ba93e29ad4ec5841ef2c4479ef0a6c633c4e2629e0508453577b"},
{file = "safetensors-0.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c3c9f0ca510e0de95abd6424789dcbc879942a3a4e29b0dfa99d9427bf1da75c"},
{file = "safetensors-0.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:88b4653059c903015284a9722f9a46838c654257173b279c8f6f46dbe80b612d"},
{file = "safetensors-0.4.1-cp310-none-win32.whl", hash = "sha256:2fe6926110e3d425c4b684a4379b7796fdc26ad7d16922ea1696c8e6ea7e920f"},
{file = "safetensors-0.4.1-cp310-none-win_amd64.whl", hash = "sha256:a79e16222106b2f5edbca1b8185661477d8971b659a3c814cc6f15181a9b34c8"},
{file = "safetensors-0.4.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:d93321eea0dd7e81b283e47a1d20dee6069165cc158286316d0d06d340de8fe8"},
{file = "safetensors-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ff8e41c8037db17de0ea2a23bc684f43eaf623be7d34906fe1ac10985b8365e"},
{file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39d36f1d88468a87c437a1bc27c502e71b6ca44c385a9117a9f9ba03a75cc9c6"},
{file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ef010e9afcb4057fb6be3d0a0cfa07aac04fe97ef73fe4a23138d8522ba7c17"},
{file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b287304f2b2220d51ccb51fd857761e78bcffbeabe7b0238f8dc36f2edfd9542"},
{file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e09000b2599e1836314430f81a3884c66a5cbabdff5d9f175b5d560d4de38d78"},
{file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c80ce0001efa16066358d2dd77993adc25f5a6c61850e4ad096a2232930bce"},
{file = "safetensors-0.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:413e1f6ac248f7d1b755199a06635e70c3515493d3b41ba46063dec33aa2ebb7"},
{file = "safetensors-0.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3ac139377cfe71ba04573f1cda66e663b7c3e95be850e9e6c2dd4b5984bd513"},
{file = "safetensors-0.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:04157d008385bea66d12fe90844a80d4a76dc25ec5230b5bd9a630496d1b7c03"},
{file = "safetensors-0.4.1-cp311-none-win32.whl", hash = "sha256:5f25297148ec665f0deb8bd67e9564634d8d6841041ab5393ccfe203379ea88b"},
{file = "safetensors-0.4.1-cp311-none-win_amd64.whl", hash = "sha256:b2f8877990a72ff595507b80f4b69036a9a1986a641f8681adf3425d97d3d2a5"},
{file = "safetensors-0.4.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:eb2c1da1cc39509d1a55620a5f4d14f8911c47a89c926a96e6f4876e864375a3"},
{file = "safetensors-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:303d2c0415cf15a28f8d7f17379ea3c34c2b466119118a34edd9965983a1a8a6"},
{file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4cb3e37a9b961ddd68e873b29fe9ab4a081e3703412e34aedd2b7a8e9cafd9"},
{file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae5497adc68669db2fed7cb2dad81e6a6106e79c9a132da3efdb6af1db1014fa"},
{file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b30abd0cddfe959d1daedf92edcd1b445521ebf7ddefc20860ed01486b33c90"},
{file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d784a98c492c751f228a4a894c3b8a092ff08b24e73b5568938c28b8c0e8f8df"},
{file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57a5ab08b0ec7a7caf30d2ac79bb30c89168431aca4f8854464bb9461686925"},
{file = "safetensors-0.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:edcf3121890b5f0616aa5a54683b1a5d2332037b970e507d6bb7841a3a596556"},
{file = "safetensors-0.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fdb58dee173ef33634c3016c459d671ca12d11e6acf9db008261cbe58107e579"},
{file = "safetensors-0.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:780dc21eb3fd32ddd0e8c904bdb0290f2454f4ac21ae71e94f9ce72db1900a5a"},
{file = "safetensors-0.4.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:48901bd540f8a3c1791314bc5c8a170927bf7f6acddb75bf0a263d081a3637d4"},
{file = "safetensors-0.4.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3b0b7b2d5976fbed8a05e2bbdce5816a59e6902e9e7c7e07dc723637ed539787"},
{file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f69903ff49cb30b9227fb5d029bea276ea20d04b06803877a420c5b1b74c689"},
{file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ddd050e01f3e843aa8c1c27bf68675b8a08e385d0045487af4d70418c3cb356"},
{file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a82bc2bd7a9a0e08239bdd6d7774d64121f136add93dfa344a2f1a6d7ef35fa"},
{file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ace9e66a40f98a216ad661245782483cf79cf56eb2b112650bb904b0baa9db5"},
{file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82cbb8f4d022f2e94498cbefca900698b8ded3d4f85212f47da614001ff06652"},
{file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:791edc10a3c359a2f5f52d5cddab0df8a45107d91027d86c3d44e57162e5d934"},
{file = "safetensors-0.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:83c2cfbe8c6304f0891e7bb378d56f66d2148972eeb5f747cd8a2246886f0d8c"},
{file = "safetensors-0.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:04dd14f53f5500eb4c4149674216ba1000670efbcf4b1b5c2643eb244e7882ea"},
{file = "safetensors-0.4.1-cp37-none-win32.whl", hash = "sha256:d5b3defa74f3723a388bfde2f5d488742bc4879682bd93267c09a3bcdf8f869b"},
{file = "safetensors-0.4.1-cp37-none-win_amd64.whl", hash = "sha256:25a043cbb59d4f75e9dd87fdf5c009dd8830105a2c57ace49b72167dd9808111"},
{file = "safetensors-0.4.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:3f6a520af7f2717c5ecba112041f2c8af1ca6480b97bf957aba81ed9642e654c"},
{file = "safetensors-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3807ac3b16288dffebb3474b555b56fe466baa677dfc16290dcd02dca1ab228"},
{file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b58ba13a9e82b4bc3fc221914f6ef237fe6c2adb13cede3ace64d1aacf49610"},
{file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dac4bb42f8679aadc59bd91a4c5a1784a758ad49d0912995945cd674089f628e"},
{file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911b48dc09e321a194def3a7431662ff4f03646832f3a8915bbf0f449b8a5fcb"},
{file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82571d20288c975c1b30b08deb9b1c3550f36b31191e1e81fae87669a92217d0"},
{file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da52ee0dc8ba03348ffceab767bd8230842fdf78f8a996e2a16445747143a778"},
{file = "safetensors-0.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2536b11ce665834201072e9397404170f93f3be10cca9995b909f023a04501ee"},
{file = "safetensors-0.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:998fbac99ca956c3a09fe07cc0b35fac26a521fa8865a690686d889f0ff4e4a6"},
{file = "safetensors-0.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:845be0aafabf2a60c2d482d4e93023fecffe5e5443d801d7a7741bae9de41233"},
{file = "safetensors-0.4.1-cp38-none-win32.whl", hash = "sha256:ce7a28bc8af685a69d7e869d09d3e180a275e3281e29cf5f1c7319e231932cc7"},
{file = "safetensors-0.4.1-cp38-none-win_amd64.whl", hash = "sha256:e056fb9e22d118cc546107f97dc28b449d88274207dd28872bd668c86216e4f6"},
{file = "safetensors-0.4.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:bdc0d039e44a727824639824090bd8869535f729878fa248addd3dc01db30eae"},
{file = "safetensors-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c1b1d510c7aba71504ece87bf393ea82638df56303e371e5e2cf09d18977dd7"},
{file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd0afd95c1e497f520e680ea01e0397c0868a3a3030e128438cf6e9e3fcd671"},
{file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f603bdd8deac6726d39f41688ed353c532dd53935234405d79e9eb53f152fbfb"},
{file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8a85e3e47e0d4eebfaf9a58b40aa94f977a56050cb5598ad5396a9ee7c087c6"},
{file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0ccb5aa0f3be2727117e5631200fbb3a5b3a2b3757545a92647d6dd8be6658f"},
{file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d784938534e255473155e4d9f276ee69eb85455b6af1292172c731409bf9adee"},
{file = "safetensors-0.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a257de175c254d39ccd6a21341cd62eb7373b05c1e618a78096a56a857e0c316"},
{file = "safetensors-0.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6fd80f7794554091836d4d613d33a7d006e2b8d6ba014d06f97cebdfda744f64"},
{file = "safetensors-0.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:35803201d980efcf964b75a0a2aee97fe5e9ecc5f3ad676b38fafdfe98e0620d"},
{file = "safetensors-0.4.1-cp39-none-win32.whl", hash = "sha256:7ff8a36e0396776d3ed9a106fc9a9d7c55d4439ca9a056a24bf66d343041d3e6"},
{file = "safetensors-0.4.1-cp39-none-win_amd64.whl", hash = "sha256:bfa2e20342b81921b98edba52f8deb68843fa9c95250739a56b52ceda5ea5c61"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ae2d5a31cfb8a973a318f7c4d2cffe0bd1fe753cdf7bb41a1939d45a0a06f964"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a45dbf03e8334d3a5dc93687d98b6dc422f5d04c7d519dac09b84a3c87dd7c6"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297b359d91126c0f9d4fd17bae3cfa2fe3a048a6971b8db07db746ad92f850c"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda3d98e2bcece388232cfc551ebf063b55bdb98f65ab54df397da30efc7dcc5"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8934bdfd202ebd0697040a3dff40dd77bc4c5bbf3527ede0532f5e7fb4d970f"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:42c3710cec7e5c764c7999697516370bee39067de0aa089b7e2cfb97ac8c6b20"},
{file = "safetensors-0.4.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53134226053e56bd56e73f7db42596e7908ed79f3c9a1016e4c1dade593ac8e5"},
{file = "safetensors-0.4.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:257d59e40a1b367cb544122e7451243d65b33c3f34d822a347f4eea6fdf97fdf"},
{file = "safetensors-0.4.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d54c2f1826e790d1eb2d2512bfd0ee443f0206b423d6f27095057c7f18a0687"},
{file = "safetensors-0.4.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645b3f1138fce6e818e79d4128afa28f0657430764cc045419c1d069ff93f732"},
{file = "safetensors-0.4.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9a7ffb1e551c6df51d267f5a751f042b183df22690f6feceac8d27364fd51d7"},
{file = "safetensors-0.4.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:44e230fbbe120de564b64f63ef3a8e6ff02840fa02849d9c443d56252a1646d4"},
{file = "safetensors-0.4.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9d16b3b2fcc6fca012c74bd01b5619c655194d3e3c13e4d4d0e446eefa39a463"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5d95ea4d8b32233910734a904123bdd3979c137c461b905a5ed32511defc075f"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:dab431699b5d45e0ca043bc580651ce9583dda594e62e245b7497adb32e99809"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d8bbb7344e39cb9d4762e85c21df94ebeb03edac923dd94bb9ed8c10eac070"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1faf5111c66a6ba91f85dff2e36edaaf36e6966172703159daeef330de4ddc7b"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:660ca1d8bff6c7bc7c6b30b9b32df74ef3ab668f5df42cefd7588f0d40feadcb"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ae2f67f04ed0bb2e56fd380a8bd3eef03f609df53f88b6f5c7e89c08e52aae00"},
{file = "safetensors-0.4.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c8ed5d2c04cdc1afc6b3c28d59580448ac07732c50d94c15e14670f9c473a2ce"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2b6a2814278b6660261aa9a9aae524616de9f1ec364e3716d219b6ed8f91801f"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3cfd1ca35eacc635f0eaa894e5c5ed83ffebd0f95cac298fd430014fa7323631"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4177b456c6b0c722d82429127b5beebdaf07149d265748e97e0a34ff0b3694c8"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313e8472197bde54e3ec54a62df184c414582979da8f3916981b6a7954910a1b"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fdb4adb76e21bad318210310590de61c9f4adcef77ee49b4a234f9dc48867869"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1d568628e9c43ca15eb96c217da73737c9ccb07520fafd8a1eba3f2750614105"},
{file = "safetensors-0.4.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:573b6023a55a2f28085fc0a84e196c779b6cbef4d9e73acea14c8094fee7686f"},
{file = "safetensors-0.4.1.tar.gz", hash = "sha256:2304658e6ada81a5223225b4efe84748e760c46079bffedf7e321763cafb36c9"},
]
[package.extras]
all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"]
dev = ["safetensors[all]"]
jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"]
numpy = ["numpy (>=1.21.6)"]
paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"]
pinned-tf = ["safetensors[numpy]", "tensorflow (==2.11.0)"]
quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"]
tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"]
testing = ["h5py (>=3.7.0)", "huggingface_hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools_rust (>=1.5.2)"]
torch = ["safetensors[numpy]", "torch (>=1.10)"]
[[package]]
name = "scikit-learn"
version = "1.3.2"
description = "A set of python modules for machine learning and data mining"
optional = false
python-versions = ">=3.8"
files = [
{file = "scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05"},
{file = "scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1"},
{file = "scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a"},
{file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c"},
{file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161"},
{file = "scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c"},
{file = "scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66"},
{file = "scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157"},
{file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb"},
{file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433"},
{file = "scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b"},
{file = "scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028"},
{file = "scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5"},
{file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525"},
{file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c"},
{file = "scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107"},
{file = "scikit_learn-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93"},
{file = "scikit_learn-1.3.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073"},
{file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d"},
{file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf"},
{file = "scikit_learn-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0"},
{file = "scikit_learn-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03"},
{file = "scikit_learn-1.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e"},
{file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a"},
{file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9"},
{file = "scikit_learn-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0"},
]
[package.dependencies]
joblib = ">=1.1.1"
numpy = ">=1.17.3,<2.0"
scipy = ">=1.5.0"
threadpoolctl = ">=2.0.0"
[package.extras]
benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"]
docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"]
examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"]
tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"]
[[package]]
name = "scipy"
version = "1.11.4"
description = "Fundamental algorithms for scientific computing in Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710"},
{file = "scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41"},
{file = "scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4"},
{file = "scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56"},
{file = "scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446"},
{file = "scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3"},
{file = "scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be"},
{file = "scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8"},
{file = "scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c"},
{file = "scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff"},
{file = "scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993"},
{file = "scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd"},
{file = "scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6"},
{file = "scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d"},
{file = "scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4"},
{file = "scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79"},
{file = "scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660"},
{file = "scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97"},
{file = "scipy-1.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e619aba2df228a9b34718efb023966da781e89dd3d21637b27f2e54db0410d7"},
{file = "scipy-1.11.4-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f3cd9e7b3c2c1ec26364856f9fbe78695fe631150f94cd1c22228456404cf1ec"},
{file = "scipy-1.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d10e45a6c50211fe256da61a11c34927c68f277e03138777bdebedd933712fea"},
{file = "scipy-1.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91af76a68eeae0064887a48e25c4e616fa519fa0d38602eda7e0f97d65d57937"},
{file = "scipy-1.11.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6df1468153a31cf55ed5ed39647279beb9cfb5d3f84369453b49e4b8502394fd"},
{file = "scipy-1.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee410e6de8f88fd5cf6eadd73c135020bfbbbdfcd0f6162c36a7638a1ea8cc65"},
{file = "scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa"},
]
[package.dependencies]
numpy = ">=1.21.6,<1.28.0"
[package.extras]
dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
[[package]]
name = "sentence-transformers"
version = "2.2.2"
description = "Multilingual text embeddings"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "sentence-transformers-2.2.2.tar.gz", hash = "sha256:dbc60163b27de21076c9a30d24b5b7b6fa05141d68cf2553fa9a77bf79a29136"},
]
[package.dependencies]
huggingface-hub = ">=0.4.0"
nltk = "*"
numpy = "*"
scikit-learn = "*"
scipy = "*"
sentencepiece = "*"
torch = ">=1.6.0"
torchvision = "*"
tqdm = "*"
transformers = ">=4.6.0,<5.0.0"
[[package]]
name = "sentencepiece"
version = "0.1.99"
description = "SentencePiece python wrapper"
optional = false
python-versions = "*"
files = [
{file = "sentencepiece-0.1.99-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0eb528e70571b7c02723e5804322469b82fe7ea418c96051d0286c0fa028db73"},
{file = "sentencepiece-0.1.99-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d7fafb2c4e4659cbdf303929503f37a26eabc4ff31d3a79bf1c5a1b338caa7"},
{file = "sentencepiece-0.1.99-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be9cf5b9e404c245aeb3d3723c737ba7a8f5d4ba262ef233a431fa6c45f732a0"},
{file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baed1a26464998f9710d20e52607c29ffd4293e7c71c6a1f83f51ad0911ec12c"},
{file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9832f08bb372d4c8b567612f8eab9e36e268dff645f1c28f9f8e851be705f6d1"},
{file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019e7535108e309dae2b253a75834fc3128240aa87c00eb80732078cdc182588"},
{file = "sentencepiece-0.1.99-cp310-cp310-win32.whl", hash = "sha256:fa16a830416bb823fa2a52cbdd474d1f7f3bba527fd2304fb4b140dad31bb9bc"},
{file = "sentencepiece-0.1.99-cp310-cp310-win_amd64.whl", hash = "sha256:14b0eccb7b641d4591c3e12ae44cab537d68352e4d3b6424944f0c447d2348d5"},
{file = "sentencepiece-0.1.99-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d3c56f24183a1e8bd61043ff2c58dfecdc68a5dd8955dc13bab83afd5f76b81"},
{file = "sentencepiece-0.1.99-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed6ea1819fd612c989999e44a51bf556d0ef6abfb553080b9be3d347e18bcfb7"},
{file = "sentencepiece-0.1.99-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2a0260cd1fb7bd8b4d4f39dc2444a8d5fd4e0a0c4d5c899810ef1abf99b2d45"},
{file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a1abff4d1ff81c77cac3cc6fefa34fa4b8b371e5ee51cb7e8d1ebc996d05983"},
{file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:004e6a621d4bc88978eecb6ea7959264239a17b70f2cbc348033d8195c9808ec"},
{file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db361e03342c41680afae5807590bc88aa0e17cfd1a42696a160e4005fcda03b"},
{file = "sentencepiece-0.1.99-cp311-cp311-win32.whl", hash = "sha256:2d95e19168875b70df62916eb55428a0cbcb834ac51d5a7e664eda74def9e1e0"},
{file = "sentencepiece-0.1.99-cp311-cp311-win_amd64.whl", hash = "sha256:f90d73a6f81248a909f55d8e6ef56fec32d559e1e9af045f0b0322637cb8e5c7"},
{file = "sentencepiece-0.1.99-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:62e24c81e74bd87a6e0d63c51beb6527e4c0add67e1a17bac18bcd2076afcfeb"},
{file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57efcc2d51caff20d9573567d9fd3f854d9efe613ed58a439c78c9f93101384a"},
{file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a904c46197993bd1e95b93a6e373dca2f170379d64441041e2e628ad4afb16f"},
{file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d89adf59854741c0d465f0e1525b388c0d174f611cc04af54153c5c4f36088c4"},
{file = "sentencepiece-0.1.99-cp36-cp36m-win32.whl", hash = "sha256:47c378146928690d1bc106fdf0da768cebd03b65dd8405aa3dd88f9c81e35dba"},
{file = "sentencepiece-0.1.99-cp36-cp36m-win_amd64.whl", hash = "sha256:9ba142e7a90dd6d823c44f9870abdad45e6c63958eb60fe44cca6828d3b69da2"},
{file = "sentencepiece-0.1.99-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7b1a9ae4d7c6f1f867e63370cca25cc17b6f4886729595b885ee07a58d3cec3"},
{file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0f644c9d4d35c096a538507b2163e6191512460035bf51358794a78515b74f7"},
{file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8843d23a0f686d85e569bd6dcd0dd0e0cbc03731e63497ca6d5bacd18df8b85"},
{file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6f690a1caebb4867a2e367afa1918ad35be257ecdb3455d2bbd787936f155"},
{file = "sentencepiece-0.1.99-cp37-cp37m-win32.whl", hash = "sha256:8a321866c2f85da7beac74a824b4ad6ddc2a4c9bccd9382529506d48f744a12c"},
{file = "sentencepiece-0.1.99-cp37-cp37m-win_amd64.whl", hash = "sha256:c42f753bcfb7661c122a15b20be7f684b61fc8592c89c870adf52382ea72262d"},
{file = "sentencepiece-0.1.99-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b476406da69c70586f0bb682fcca4c9b40e5059814f2db92303ea4585c650c"},
{file = "sentencepiece-0.1.99-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfbcfe13c69d3f87b7fcd5da168df7290a6d006329be71f90ba4f56bc77f8561"},
{file = "sentencepiece-0.1.99-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:445b0ec381af1cd4eef95243e7180c63d9c384443c16c4c47a28196bd1cda937"},
{file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6890ea0f2b4703f62d0bf27932e35808b1f679bdb05c7eeb3812b935ba02001"},
{file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb71af492b0eefbf9f2501bec97bcd043b6812ab000d119eaf4bd33f9e283d03"},
{file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b866b5bd3ddd54166bbcbf5c8d7dd2e0b397fac8537991c7f544220b1f67bc"},
{file = "sentencepiece-0.1.99-cp38-cp38-win32.whl", hash = "sha256:b133e8a499eac49c581c3c76e9bdd08c338cc1939e441fee6f92c0ccb5f1f8be"},
{file = "sentencepiece-0.1.99-cp38-cp38-win_amd64.whl", hash = "sha256:0eaf3591dd0690a87f44f4df129cf8d05d8a4029b5b6709b489b8e27f9a9bcff"},
{file = "sentencepiece-0.1.99-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38efeda9bbfb55052d482a009c6a37e52f42ebffcea9d3a98a61de7aee356a28"},
{file = "sentencepiece-0.1.99-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c030b081dc1e1bcc9fadc314b19b740715d3d566ad73a482da20d7d46fd444c"},
{file = "sentencepiece-0.1.99-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84dbe53e02e4f8a2e45d2ac3e430d5c83182142658e25edd76539b7648928727"},
{file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b0f55d0a0ee1719b4b04221fe0c9f0c3461dc3dabd77a035fa2f4788eb3ef9a"},
{file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e800f206cd235dc27dc749299e05853a4e4332e8d3dfd81bf13d0e5b9007d9"},
{file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae1c40cda8f9d5b0423cfa98542735c0235e7597d79caf318855cdf971b2280"},
{file = "sentencepiece-0.1.99-cp39-cp39-win32.whl", hash = "sha256:c84ce33af12ca222d14a1cdd37bd76a69401e32bc68fe61c67ef6b59402f4ab8"},
{file = "sentencepiece-0.1.99-cp39-cp39-win_amd64.whl", hash = "sha256:350e5c74d739973f1c9643edb80f7cc904dc948578bcb1d43c6f2b173e5d18dd"},
{file = "sentencepiece-0.1.99.tar.gz", hash = "sha256:189c48f5cb2949288f97ccdb97f0473098d9c3dcf5a3d99d4eabe719ec27297f"},
]
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "1.39.2" version = "1.39.2"
@ -3542,17 +2893,6 @@ files = [
[package.extras] [package.extras]
doc = ["reno", "sphinx", "tornado (>=4.5)"] doc = ["reno", "sphinx", "tornado (>=4.5)"]
[[package]]
name = "threadpoolctl"
version = "3.2.0"
description = "threadpoolctl"
optional = false
python-versions = ">=3.8"
files = [
{file = "threadpoolctl-3.2.0-py3-none-any.whl", hash = "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032"},
{file = "threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355"},
]
[[package]] [[package]]
name = "tiktoken" name = "tiktoken"
version = "0.5.2" version = "0.5.2"
@ -3720,97 +3060,6 @@ dev = ["tokenizers[testing]"]
docs = ["setuptools_rust", "sphinx", "sphinx_rtd_theme"] docs = ["setuptools_rust", "sphinx", "sphinx_rtd_theme"]
testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"]
[[package]]
name = "torch"
version = "2.1.2"
description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "torch-2.1.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:3a871edd6c02dae77ad810335c0833391c1a4ce49af21ea8cf0f6a5d2096eea8"},
{file = "torch-2.1.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:bef6996c27d8f6e92ea4e13a772d89611da0e103b48790de78131e308cf73076"},
{file = "torch-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:0e13034fd5fb323cbbc29e56d0637a3791e50dd589616f40c79adfa36a5a35a1"},
{file = "torch-2.1.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:d9b535cad0df3d13997dbe8bd68ac33e0e3ae5377639c9881948e40794a61403"},
{file = "torch-2.1.2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:f9a55d55af02826ebfbadf4e9b682f0f27766bc33df8236b48d28d705587868f"},
{file = "torch-2.1.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:a6ebbe517097ef289cc7952783588c72de071d4b15ce0f8b285093f0916b1162"},
{file = "torch-2.1.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:8f32ce591616a30304f37a7d5ea80b69ca9e1b94bba7f308184bf616fdaea155"},
{file = "torch-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e0ee6cf90c8970e05760f898d58f9ac65821c37ffe8b04269ec787aa70962b69"},
{file = "torch-2.1.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:76d37967c31c99548ad2c4d3f2cf191db48476f2e69b35a0937137116da356a1"},
{file = "torch-2.1.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:e2d83f07b4aac983453ea5bf8f9aa9dacf2278a8d31247f5d9037f37befc60e4"},
{file = "torch-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f41fe0c7ecbf903a568c73486139a75cfab287a0f6c17ed0698fdea7a1e8641d"},
{file = "torch-2.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e3225f47d50bb66f756fe9196a768055d1c26b02154eb1f770ce47a2578d3aa7"},
{file = "torch-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33d59cd03cb60106857f6c26b36457793637512998666ee3ce17311f217afe2b"},
{file = "torch-2.1.2-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:8e221deccd0def6c2badff6be403e0c53491805ed9915e2c029adbcdb87ab6b5"},
{file = "torch-2.1.2-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:05b18594f60a911a0c4f023f38a8bda77131fba5fd741bda626e97dcf5a3dd0a"},
{file = "torch-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:9ca96253b761e9aaf8e06fb30a66ee301aecbf15bb5a303097de1969077620b6"},
{file = "torch-2.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d93ba70f67b08c2ae5598ee711cbc546a1bc8102cef938904b8c85c2089a51a0"},
{file = "torch-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:255b50bc0608db177e6a3cc118961d77de7e5105f07816585fa6f191f33a9ff3"},
{file = "torch-2.1.2-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6984cd5057c0c977b3c9757254e989d3f1124f4ce9d07caa6cb637783c71d42a"},
{file = "torch-2.1.2-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:bc195d7927feabc0eb7c110e457c955ed2ab616f3c7c28439dd4188cf589699f"},
]
[package.dependencies]
filelock = "*"
fsspec = "*"
jinja2 = "*"
networkx = "*"
nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-nccl-cu12 = {version = "2.18.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
sympy = "*"
triton = {version = "2.1.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
typing-extensions = "*"
[package.extras]
dynamo = ["jinja2"]
opt-einsum = ["opt-einsum (>=3.3)"]
[[package]]
name = "torchvision"
version = "0.16.2"
description = "image and video datasets and models for torch deep learning"
optional = false
python-versions = ">=3.8"
files = [
{file = "torchvision-0.16.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:bc86f2800cb2c0c1a09c581409cdd6bff66e62f103dc83fc63f73346264c3756"},
{file = "torchvision-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b024bd412df6d3a007dcebf311a894eb3c5c21e1af80d12be382bbcb097a7c3a"},
{file = "torchvision-0.16.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:e89f10f3c8351972b6e3fda95bc3e479ea8dbfc9dfcfd2c32902dbad4ba5cfc5"},
{file = "torchvision-0.16.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:96c7583700112a410bdc4e1e4f118c429dab49c29c9a31a2cc3579bc9b08b19d"},
{file = "torchvision-0.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4032ebb3277fb07ff6a9b818d50a547fb8fcd89d958cfd9e773322454bb688"},
{file = "torchvision-0.16.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:67b1aaf8b8cb02ce75dd445f291a27c8036a502f8c0aa76e28c37a0faac2e153"},
{file = "torchvision-0.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bef30d03e1d1c629761f4dca51d3b7d8a0dc0acce6f4068ab2a1634e8e7b64e0"},
{file = "torchvision-0.16.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e59cc7b2bd1ab5c0ce4ae382e4e37be8f1c174e8b5de2f6a23c170de9ae28495"},
{file = "torchvision-0.16.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:e130b08cc9b3cc73a6c59d6edf032394a322f9579bfd21d14bc2e1d0999aa758"},
{file = "torchvision-0.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:8692ab1e48807e9604046a6f4beeb67b523294cee1b00828654bb0df2cfce2b2"},
{file = "torchvision-0.16.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:b82732dcf876a37c852772342aa6ee3480c03bb3e2a802ae109fc5f7e28d26e9"},
{file = "torchvision-0.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4b065143d1a720fe8a9077fd4be35d491f98819ec80b3dbbc3ec64d0b707a906"},
{file = "torchvision-0.16.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bc5f274e4ecd1b86062063cdf4fd385a1d39d147a3a2685fbbde9ff08bb720b8"},
{file = "torchvision-0.16.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:335959c43b371c0474af34c1ef2a52efdc7603c45700d29e4475eeb02984170c"},
{file = "torchvision-0.16.2-cp38-cp38-win_amd64.whl", hash = "sha256:7fd22d86e08eba321af70cad291020c2cdeac069b00ce88b923ca52e06174769"},
{file = "torchvision-0.16.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:56115268b37f0b75364e3654e47ad9abc66ac34c1f9e5e3dfa89a22d6a40017a"},
{file = "torchvision-0.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82805f8445b094f9d1e770390ee6cc86855e89955e08ce34af2e2274fc0e5c45"},
{file = "torchvision-0.16.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3f4bd5fcbc361476e2e78016636ac7d5509e59d9962521f06eb98e6803898182"},
{file = "torchvision-0.16.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8199acdf8ab066a28b84a5b6f4d97b58976d9e164b1acc3a9d14fccfaf74bb3a"},
{file = "torchvision-0.16.2-cp39-cp39-win_amd64.whl", hash = "sha256:41dd4fa9f176d563fe9f1b9adef3b7e582cdfb60ce8c9bc51b094a025be687c9"},
]
[package.dependencies]
numpy = "*"
pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0"
requests = "*"
torch = "2.1.2"
[package.extras]
scipy = ["scipy"]
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.1" version = "4.66.1"
@ -3831,99 +3080,6 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
telegram = ["requests"] telegram = ["requests"]
[[package]]
name = "transformers"
version = "4.36.2"
description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "transformers-4.36.2-py3-none-any.whl", hash = "sha256:462066c4f74ee52516f12890dcc9ec71d1a5e97998db621668455117a54330f6"},
{file = "transformers-4.36.2.tar.gz", hash = "sha256:d8068e897e47793281501e547d2bbdfc5b8556409c2cb6c3d9e2ca77d4c0b4ec"},
]
[package.dependencies]
filelock = "*"
huggingface-hub = ">=0.19.3,<1.0"
numpy = ">=1.17"
packaging = ">=20.0"
pyyaml = ">=5.1"
regex = "!=2019.12.17"
requests = "*"
safetensors = ">=0.3.1"
tokenizers = ">=0.14,<0.19"
tqdm = ">=4.27"
[package.extras]
accelerate = ["accelerate (>=0.21.0)"]
agents = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "datasets (!=2.5.0)", "diffusers", "opencv-python", "sentencepiece (>=0.1.91,!=0.1.92)", "torch (>=1.10,!=1.12.0)"]
all = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.6,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm", "tokenizers (>=0.14,<0.19)", "torch (>=1.10,!=1.12.0)", "torchaudio", "torchvision"]
audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"]
codecarbon = ["codecarbon (==1.2.0)"]
deepspeed = ["accelerate (>=0.21.0)", "deepspeed (>=0.9.3)"]
deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.21.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "optuna", "parameterized", "protobuf", "psutil", "pydantic (<2)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.1.5)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"]
dev = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "beautifulsoup4", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "decord (==0.6.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (<2)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.1.5)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>=2.6,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm", "tokenizers (>=0.14,<0.19)", "torch (>=1.10,!=1.12.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"]
dev-tensorflow = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (<2)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.1.5)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "tensorflow (>=2.6,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.14,<0.19)", "urllib3 (<2.0.0)"]
dev-torch = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "beautifulsoup4", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (<2)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.1.5)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm", "tokenizers (>=0.14,<0.19)", "torch (>=1.10,!=1.12.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"]
docs = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1,<=0.7.0)", "hf-doc-builder", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.6,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm", "tokenizers (>=0.14,<0.19)", "torch (>=1.10,!=1.12.0)", "torchaudio", "torchvision"]
docs-specific = ["hf-doc-builder"]
flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)"]
flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"]
ftfy = ["ftfy"]
integrations = ["optuna", "ray[tune] (>=2.7.0)", "sigopt"]
ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
modelcreation = ["cookiecutter (==1.7.3)"]
natten = ["natten (>=0.14.6)"]
onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"]
onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"]
optuna = ["optuna"]
quality = ["GitPython (<3.1.19)", "datasets (!=2.5.0)", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "ruff (==0.1.5)", "urllib3 (<2.0.0)"]
ray = ["ray[tune] (>=2.7.0)"]
retrieval = ["datasets (!=2.5.0)", "faiss-cpu"]
sagemaker = ["sagemaker (>=2.31.0)"]
sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"]
serving = ["fastapi", "pydantic (<2)", "starlette", "uvicorn"]
sigopt = ["sigopt"]
sklearn = ["scikit-learn"]
speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"]
testing = ["GitPython (<3.1.19)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "parameterized", "protobuf", "psutil", "pydantic (<2)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.1.5)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "tensorboard", "timeout-decorator"]
tf = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow (>=2.6,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"]
tf-cpu = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow-cpu (>=2.6,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"]
tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"]
timm = ["timm"]
tokenizers = ["tokenizers (>=0.14,<0.19)"]
torch = ["accelerate (>=0.21.0)", "torch (>=1.10,!=1.12.0)"]
torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"]
torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"]
torchhub = ["filelock", "huggingface-hub (>=0.19.3,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.14,<0.19)", "torch (>=1.10,!=1.12.0)", "tqdm (>=4.27)"]
video = ["av (==9.2.0)", "decord (==0.6.0)"]
vision = ["Pillow (>=10.0.1,<=15.0)"]
[[package]]
name = "triton"
version = "2.1.0"
description = "A language and compiler for custom Deep Learning operations"
optional = false
python-versions = "*"
files = [
{file = "triton-2.1.0-0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:66439923a30d5d48399b08a9eae10370f6c261a5ec864a64983bae63152d39d7"},
{file = "triton-2.1.0-0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:919b06453f0033ea52c13eaf7833de0e57db3178d23d4e04f9fc71c4f2c32bf8"},
{file = "triton-2.1.0-0-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae4bb8a91de790e1866405211c4d618379781188f40d5c4c399766914e84cd94"},
{file = "triton-2.1.0-0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39f6fb6bdccb3e98f3152e3fbea724f1aeae7d749412bbb1fa9c441d474eba26"},
{file = "triton-2.1.0-0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21544e522c02005a626c8ad63d39bdff2f31d41069592919ef281e964ed26446"},
{file = "triton-2.1.0-0-pp37-pypy37_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:143582ca31dd89cd982bd3bf53666bab1c7527d41e185f9e3d8a3051ce1b663b"},
{file = "triton-2.1.0-0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82fc5aeeedf6e36be4e4530cbdcba81a09d65c18e02f52dc298696d45721f3bd"},
{file = "triton-2.1.0-0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81a96d110a738ff63339fc892ded095b31bd0d205e3aace262af8400d40b6fa8"},
]
[package.dependencies]
filelock = "*"
[package.extras]
build = ["cmake (>=3.18)", "lit"]
tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)"]
tutorials = ["matplotlib", "pandas", "tabulate"]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.9.0" version = "0.9.0"
@ -4441,4 +3597,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "76237f0e04218f9ca9a2593ccf952452bd6d45657066feec87373279fb7fe6a2" content-hash = "f1dfc749e30bbf23ec06ac8b236ef6c2072591fe9a8a56f26e05416151ca97bb"

View file

@ -34,7 +34,6 @@ langchain = "0.1.0"
tiktoken = "^0.5.2" tiktoken = "^0.5.2"
openai = "^1.7.1" openai = "^1.7.1"
chromadb = "^0.4.22" chromadb = "^0.4.22"
sentence-transformers = "^2.2.2"
python-dotenv = "^1.0.0" python-dotenv = "^1.0.0"
redis = "^5.0.1" redis = "^5.0.1"
langchain-community = "^0.0.11" langchain-community = "^0.0.11"

View file

@ -24,6 +24,5 @@ langchain-openai
tiktoken tiktoken
openai openai
chromadb chromadb
sentence-transformers
python-dotenv python-dotenv
redis redis

View file

@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
from sqlalchemy import JSON, BigInteger, Column, ForeignKey from sqlalchemy import JSON, Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
@ -38,12 +38,12 @@ class ActivityBase(SQLModel):
class Activity(ActivityBase, table=True): class Activity(ActivityBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
course_id: int = Field( course_id: int = Field(
default=None, default=None,
sa_column=Column( sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")),
BigInteger, ForeignKey("course.id", ondelete="CASCADE")
),
) )
activity_uuid: str = "" activity_uuid: str = ""
creation_date: str = "" creation_date: str = ""

View file

@ -21,7 +21,7 @@ class BlockBase(SQLModel):
class Block(BlockBase, table=True): class Block(BlockBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
content: dict = Field(default={}, sa_column=Column(JSON)) content: dict = Field(default={}, sa_column=Column(JSON))
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(sa_column= Column("org_id", ForeignKey("organization.id", ondelete="CASCADE")))
course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))) course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE")))
chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE"))) chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE"))) activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")))

View file

@ -1,4 +1,5 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@ -10,7 +11,9 @@ class CollectionBase(SQLModel):
class Collection(CollectionBase, table=True): class Collection(CollectionBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
collection_uuid: str = "" collection_uuid: str = ""
creation_date: str = "" creation_date: str = ""
update_date: str = "" update_date: str = ""

View file

@ -1,12 +1,12 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class CollectionCourse(SQLModel, table=True): class CollectionCourse(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE"))) collection_id: int = Field(sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE")))
course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))) course_id: int = Field(sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")))
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@ -7,10 +7,10 @@ class CourseChapter(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
order: int order: int
course_id: int = Field( course_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")) sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
) )
chapter_id: int = Field( chapter_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")) sa_column=Column(Integer, ForeignKey("chapter.id", ondelete="CASCADE"))
) )
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str creation_date: str

View file

@ -1,4 +1,5 @@
from typing import List, Optional from typing import List, Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.users import UserRead from src.db.users import UserRead
from src.db.trails import TrailRead from src.db.trails import TrailRead
@ -17,7 +18,9 @@ class CourseBase(SQLModel):
class Course(CourseBase, table=True): class Course(CourseBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
course_uuid: str = "" course_uuid: str = ""
creation_date: str = "" creation_date: str = ""
update_date: str = "" update_date: str = ""

View file

@ -21,8 +21,8 @@ class AIConfig(BaseModel):
enabled : bool = True enabled : bool = True
limits: AILimitsSettings = AILimitsSettings() limits: AILimitsSettings = AILimitsSettings()
embeddings: Literal[ embeddings: Literal[
"text-embedding-ada-002", "all-MiniLM-L6-v2" "text-embedding-ada-002",
] = "all-MiniLM-L6-v2" ] = "text-embedding-ada-002"
ai_model: Literal["gpt-3.5-turbo", "gpt-4-1106-preview"] = "gpt-3.5-turbo" ai_model: Literal["gpt-3.5-turbo", "gpt-4-1106-preview"] = "gpt-3.5-turbo"
features: AIEnabledFeatures = AIEnabledFeatures() features: AIEnabledFeatures = AIEnabledFeatures()

View file

@ -1,5 +1,8 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.roles import RoleRead
from src.db.organization_config import OrganizationConfig from src.db.organization_config import OrganizationConfig
@ -32,3 +35,9 @@ class OrganizationRead(OrganizationBase):
config: Optional[OrganizationConfig | dict] config: Optional[OrganizationConfig | dict]
creation_date: str creation_date: str
update_date: str update_date: str
class OrganizationUser(BaseModel):
from src.db.users import UserRead
user: UserRead
role: RoleRead

View file

@ -1,5 +1,6 @@
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@ -12,7 +13,9 @@ class ResourceAuthorshipEnum(str, Enum):
class ResourceAuthor(SQLModel, table=True): class ResourceAuthor(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
resource_uuid: str resource_uuid: str
user_id: int = Field(default=None, foreign_key="user.id") user_id: int = Field(
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
)
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
creation_date: str = "" creation_date: str = ""
update_date: str = "" update_date: str = ""

View file

@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
from typing import Optional, Union from typing import Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@ -45,7 +45,10 @@ class RoleBase(SQLModel):
class Role(RoleBase, table=True): class Role(RoleBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id") org_id: Optional[int] = Field(
default=None,
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
role_uuid: str = "" role_uuid: str = ""
creation_date: str = "" creation_date: str = ""

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
@ -23,10 +23,18 @@ class TrailRun(SQLModel, table=True):
data: dict = Field(default={}, sa_column=Column(JSON)) data: dict = Field(default={}, sa_column=Column(JSON))
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
# foreign keys # foreign keys
trail_id: int = Field(default=None, foreign_key="trail.id") trail_id: int = Field(
course_id: int = Field(default=None, foreign_key="course.id") sa_column=Column(Integer, ForeignKey("trail.id", ondelete="CASCADE"))
org_id: int = Field(default=None, foreign_key="organization.id") )
user_id: int = Field(default=None, foreign_key="user.id") course_id: int = Field(
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
)
org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
user_id: int = Field(
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
)
# timestamps # timestamps
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from sqlalchemy import BigInteger, ForeignKey, JSON, Column from sqlalchemy import ForeignKey, JSON, Column, Integer
class TrailStepTypeEnum(str, Enum): class TrailStepTypeEnum(str, Enum):
@ -18,13 +18,23 @@ class TrailStep(SQLModel, table=True):
data: dict = Field(default={}, sa_column=Column(JSON)) data: dict = Field(default={}, sa_column=Column(JSON))
# foreign keys # foreign keys
trailrun_id: int = Field( trailrun_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("trailrun.id", ondelete="CASCADE")) sa_column=Column(Integer, ForeignKey("trailrun.id", ondelete="CASCADE"))
)
trail_id: int = Field(
sa_column=Column(Integer, ForeignKey("trail.id", ondelete="CASCADE"))
)
activity_id: int = Field(
sa_column=Column(Integer, ForeignKey("activity.id", ondelete="CASCADE"))
)
course_id: int = Field(
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
)
org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
user_id: int = Field(
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
) )
trail_id: int = Field(default=None, foreign_key="trail.id")
activity_id: int = Field(default=None, foreign_key="activity.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# timestamps # timestamps
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.trail_runs import TrailRunRead from src.db.trail_runs import TrailRunRead
@ -24,8 +25,12 @@ class TrailCreate(TrailBase):
class TrailRead(BaseModel): class TrailRead(BaseModel):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
trail_uuid: Optional[str] trail_uuid: Optional[str]
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(
user_id: int = Field(default=None, foreign_key="user.id") sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
user_id: int = Field(
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
)
creation_date: Optional[str] creation_date: Optional[str]
update_date: Optional[str] update_date: Optional[str]
runs: list[TrailRunRead] runs: list[TrailRunRead]

View file

@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@ -7,7 +7,7 @@ class UserOrganization(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(default=None, foreign_key="user.id") user_id: int = Field(default=None, foreign_key="user.id")
org_id: int = Field( org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE")) sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
) )
role_id: int = Field(default=None, foreign_key="role.id") role_id: int = Field(default=None, foreign_key="role.id")
creation_date: str creation_date: str

View file

@ -1,9 +1,8 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.roles import RoleRead from src.db.roles import RoleRead
from src.db.organizations import OrganizationRead
class UserBase(SQLModel): class UserBase(SQLModel):
@ -45,6 +44,7 @@ class PublicUser(UserRead):
class UserRoleWithOrg(BaseModel): class UserRoleWithOrg(BaseModel):
from src.db.organizations import OrganizationRead
role: RoleRead role: RoleRead
org: OrganizationRead org: OrganizationRead

View file

@ -1,3 +1,4 @@
from datetime import timedelta
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session from sqlmodel import Session
@ -10,7 +11,7 @@ from src.security.auth import AuthJWT, authenticate_user
router = APIRouter() router = APIRouter()
@router.post("/refresh") @router.get("/refresh")
def refresh(response: Response, Authorize: AuthJWT = Depends()): def refresh(response: Response, Authorize: AuthJWT = Depends()):
""" """
The jwt_refresh_token_required() function insures a valid refresh The jwt_refresh_token_required() function insures a valid refresh
@ -28,6 +29,7 @@ def refresh(response: Response, Authorize: AuthJWT = Depends()):
value=new_access_token, value=new_access_token,
httponly=False, httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain, domain=get_learnhouse_config().hosting_config.cookie_config.domain,
expires=int(timedelta(hours=8).total_seconds()),
) )
return {"access_token": new_access_token} return {"access_token": new_access_token}
@ -53,14 +55,16 @@ async def login(
access_token = Authorize.create_access_token(subject=form_data.username) access_token = Authorize.create_access_token(subject=form_data.username)
refresh_token = Authorize.create_refresh_token(subject=form_data.username) refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token) Authorize.set_refresh_cookies(refresh_token)
# set cookies using fastapi # set cookies using fastapi
response.set_cookie( response.set_cookie(
key="access_token_cookie", key="access_token_cookie",
value=access_token, value=access_token,
httponly=False, httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain, domain=get_learnhouse_config().hosting_config.cookie_config.domain,
expires=int(timedelta(hours=8).total_seconds()),
) )
user = UserRead.from_orm(user) user = UserRead.from_orm(user)
result = { result = {

View file

@ -31,8 +31,8 @@ async def api_create_course(
name: str = Form(), name: str = Form(),
description: str = Form(), description: str = Form(),
public: bool = Form(), public: bool = Form(),
learnings: str = Form(), learnings: str = Form(None),
tags: str = Form(), tags: str = Form(None),
about: str = Form(), about: str = Form(),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),

View file

@ -1,6 +1,20 @@
from typing import List from typing import List, Literal
from fastapi import APIRouter, Depends, Request, UploadFile from fastapi import APIRouter, Depends, Request, UploadFile
from sqlmodel import Session from sqlmodel import Session
from src.services.orgs.invites import (
create_invite_code,
delete_invite_code,
get_invite_code,
get_invite_codes,
)
from src.services.orgs.users import (
get_list_of_invited_users,
get_organization_users,
invite_batch_users,
remove_invited_user,
remove_user_from_org,
update_user_role,
)
from src.db.organization_config import OrganizationConfigBase from src.db.organization_config import OrganizationConfigBase
from src.db.users import PublicUser from src.db.users import PublicUser
from src.db.organizations import ( from src.db.organizations import (
@ -8,6 +22,7 @@ from src.db.organizations import (
OrganizationCreate, OrganizationCreate,
OrganizationRead, OrganizationRead,
OrganizationUpdate, OrganizationUpdate,
OrganizationUser,
) )
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.security.auth import get_current_user from src.security.auth import get_current_user
@ -20,6 +35,7 @@ from src.services.orgs.orgs import (
get_orgs_by_user, get_orgs_by_user,
update_org, update_org,
update_org_logo, update_org_logo,
update_org_signup_mechanism,
) )
@ -69,6 +85,166 @@ async def api_get_org(
return await get_organization(request, org_id, db_session, current_user) return await get_organization(request, org_id, db_session, current_user)
@router.get("/{org_id}/users")
async def api_get_org_users(
request: Request,
org_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> list[OrganizationUser]:
"""
Get single Org by ID
"""
return await get_organization_users(request, org_id, db_session, current_user)
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
async def api_update_user_role(
request: Request,
org_id: str,
user_id: str,
role_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Update user role
"""
return await update_user_role(
request, org_id, user_id, role_uuid, db_session, current_user
)
@router.delete("/{org_id}/users/{user_id}")
async def api_remove_user_from_org(
request: Request,
org_id: int,
user_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Remove user from org
"""
return await remove_user_from_org(
request, org_id, user_id, db_session, current_user
)
# Config related routes
@router.put("/{org_id}/signup_mechanism")
async def api_get_org_signup_mechanism(
request: Request,
org_id: int,
signup_mechanism: Literal["open", "inviteOnly"],
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get org signup mechanism
"""
return await update_org_signup_mechanism(
request, signup_mechanism, org_id, current_user, db_session
)
# Invites related routes
@router.post("/{org_id}/invites")
async def api_create_invite_code(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Create invite code
"""
return await create_invite_code(request, org_id, current_user, db_session)
@router.get("/{org_id}/invites")
async def api_get_invite_codes(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get invite codes
"""
return await get_invite_codes(request, org_id, current_user, db_session)
@router.get("/{org_id}/invites/code/{invite_code}")
async def api_get_invite_code(
request: Request,
org_id: int,
invite_code: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get invite code
"""
print(f"org_id: {org_id}, invite_code: {invite_code}")
return await get_invite_code(request, org_id,invite_code, current_user, db_session)
@router.delete("/{org_id}/invites/{org_invite_code_uuid}")
async def api_delete_invite_code(
request: Request,
org_id: int,
org_invite_code_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Delete invite code
"""
return await delete_invite_code(
request, org_id, org_invite_code_uuid, current_user, db_session
)
@router.post("/{org_id}/invites/users/batch")
async def api_invite_batch_users(
request: Request,
org_id: int,
users: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Invite batch users
"""
return await invite_batch_users(request, org_id, users, db_session, current_user)
@router.get("/{org_id}/invites/users")
async def api_get_org_users_invites(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get org users invites
"""
return await get_list_of_invited_users(request, org_id, db_session, current_user)
@router.delete("/{org_id}/invites/users/{email}")
async def api_delete_org_users_invites(
request: Request,
org_id: int,
email: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Delete org users invites
"""
return await remove_invited_user(request, org_id, email, db_session, current_user)
@router.get("/slug/{org_slug}") @router.get("/slug/{org_slug}")
async def api_get_org_by_slug( async def api_get_org_by_slug(
request: Request, request: Request,

View file

@ -1,6 +1,7 @@
from typing import Literal from typing import Literal
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
from sqlmodel import Session from sqlmodel import Session
from src.services.orgs.orgs import get_org_join_mechanism
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
@ -16,12 +17,14 @@ from src.db.users import (
from src.services.users.users import ( from src.services.users.users import (
authorize_user_action, authorize_user_action,
create_user, create_user,
create_user_with_invite,
create_user_without_org, create_user_without_org,
delete_user_by_id, delete_user_by_id,
get_user_session, get_user_session,
read_user_by_id, read_user_by_id,
read_user_by_uuid, read_user_by_uuid,
update_user, update_user,
update_user_avatar,
update_user_password, update_user_password,
) )
@ -77,7 +80,48 @@ async def api_create_user_with_orgid(
""" """
Create User with Org ID Create User with Org ID
""" """
return await create_user(request, db_session, current_user, user_object, org_id) print(await get_org_join_mechanism(request, org_id, current_user, db_session))
# TODO(fix) : This is temporary, logic should be moved to service
if (
await get_org_join_mechanism(request, org_id, current_user, db_session)
== "inviteOnly"
):
raise HTTPException(
status_code=403,
detail="You need an invite to join this organization",
)
else:
return await create_user(request, db_session, current_user, user_object, org_id)
@router.post("/{org_id}/invite/{invite_code}", response_model=UserRead, tags=["users"])
async def api_create_user_with_orgid_and_invite(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_object: UserCreate,
invite_code: str,
org_id: int,
) -> UserRead:
"""
Create User with Org ID and invite code
"""
# TODO: This is temporary, logic should be moved to service
if (
await get_org_join_mechanism(request, org_id, current_user, db_session)
== "inviteOnly"
):
return await create_user_with_invite(
request, db_session, current_user, user_object, org_id, invite_code
)
else:
raise HTTPException(
status_code=403,
detail="This organization does not require an invite code",
)
@router.post("/", response_model=UserRead, tags=["users"]) @router.post("/", response_model=UserRead, tags=["users"])
@ -137,6 +181,20 @@ async def api_update_user(
return await update_user(request, db_session, user_id, current_user, user_object) return await update_user(request, db_session, user_id, current_user, user_object)
@router.put("/update_avatar/{user_id}", response_model=UserRead, tags=["users"])
async def api_update_avatar_user(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
avatar_file: UploadFile | None = None,
) -> UserRead:
"""
Update User
"""
return await update_user_avatar(request, db_session, current_user, avatar_file)
@router.put("/change_password/{user_id}", response_model=UserRead, tags=["users"]) @router.put("/change_password/{user_id}", response_model=UserRead, tags=["users"])
async def api_update_user_password( async def api_update_user_password(
*, *,

View file

@ -21,7 +21,9 @@ class Settings(BaseModel):
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
authjwt_token_location = {"cookies", "headers"} authjwt_token_location = {"cookies", "headers"}
authjwt_cookie_csrf_protect = False authjwt_cookie_csrf_protect = False
authjwt_access_token_expires = False if isDevModeEnabled() else 28800 authjwt_access_token_expires = (
False if isDevModeEnabled() else timedelta(hours=8).total_seconds()
)
authjwt_cookie_samesite = "lax" authjwt_cookie_samesite = "lax"
authjwt_cookie_secure = True authjwt_cookie_secure = True
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain

View file

@ -16,12 +16,11 @@ async def authorization_verify_if_element_is_public(
element_uuid: str, element_uuid: str,
action: Literal["read"], action: Literal["read"],
db_session: Session, db_session: Session,
): ):
element_nature = await check_element_type(element_uuid) element_nature = await check_element_type(element_uuid)
# Verifies if the element is public # Verifies if the element is public
if element_nature == ("courses" or "collections") and action == "read": if element_nature == ("courses") and action == "read":
if element_nature == "courses": if element_nature == "courses":
print("looking for course")
statement = select(Course).where( statement = select(Course).where(
Course.public == True, Course.course_uuid == element_uuid Course.public == True, Course.course_uuid == element_uuid
) )
@ -29,20 +28,29 @@ async def authorization_verify_if_element_is_public(
if course: if course:
return True return True
else: else:
return False raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights : You don't have the right to perform this action",
)
if element_nature == "collections" and action == "read":
if element_nature == "collections":
statement = select(Collection).where( statement = select(Collection).where(
Collection.public == True, Collection.collection_uuid == element_uuid Collection.public == True, Collection.collection_uuid == element_uuid
) )
collection = db_session.exec(statement).first() collection = db_session.exec(statement).first()
if collection: if collection:
return True return True
else: else:
return False raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights : You don't have the right to perform this action",
)
else: else:
return False raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights : You don't have the right to perform this action",
)
# Tested and working # Tested and working
@ -106,6 +114,34 @@ async def authorization_verify_based_on_roles(
return False return False
async def authorization_verify_based_on_org_admin_status(
request: Request,
user_id: int,
action: Literal["read", "update", "delete", "create"],
element_uuid: str,
db_session: Session,
):
await check_element_type(element_uuid)
# Get user roles bound to an organization and standard roles
statement = (
select(Role)
.join(UserOrganization)
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
.where(UserOrganization.user_id == user_id)
)
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
# Find in roles list if there is a role that matches users action for this type of element
for role in user_roles_in_organization_and_standard_roles:
role = Role.from_orm(role)
if role.id == 1 or role.id == 2:
return True
else:
return False
# Tested and working # Tested and working
async def authorization_verify_based_on_roles_and_authorship( async def authorization_verify_based_on_roles_and_authorship(
request: Request, request: Request,

View file

@ -5,7 +5,6 @@ async def check_element_type(element_id):
""" """
Check if the element is a course, a user, a house or a collection, by checking its prefix Check if the element is a course, a user, a house or a collection, by checking its prefix
""" """
print("element_id", element_id)
if element_id.startswith("course_"): if element_id.startswith("course_"):
return "courses" return "courses"
elif element_id.startswith("user_"): elif element_id.startswith("user_"):

View file

@ -67,8 +67,11 @@ def ai_start_activity_chat_session(
# Serialize Activity Content Blocks to a text comprehensible by the AI # Serialize Activity Content Blocks to a text comprehensible by the AI
structured = structure_activity_content_by_type(content) structured = structure_activity_content_by_type(content)
isEmpty = structured == []
ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text( ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(
structured, course, activity structured, course, activity, isActivityEmpty=isEmpty
) )
# Get Activity Organization # Get Activity Organization

View file

@ -2,7 +2,6 @@ from typing import Optional
from uuid import uuid4 from uuid import uuid4
from langchain.agents import AgentExecutor from langchain.agents import AgentExecutor
from langchain.text_splitter import CharacterTextSplitter from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings
from langchain_community.vectorstores import Chroma from langchain_community.vectorstores import Chroma
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
from langchain.prompts import MessagesPlaceholder from langchain.prompts import MessagesPlaceholder
@ -45,7 +44,6 @@ def ask_ai(
texts = text_splitter.split_documents(documents) texts = text_splitter.split_documents(documents)
embedding_models = { embedding_models = {
"all-MiniLM-L6-v2": SentenceTransformerEmbeddings,
"text-embedding-ada-002": OpenAIEmbeddings, "text-embedding-ada-002": OpenAIEmbeddings,
} }
@ -53,11 +51,11 @@ def ask_ai(
if embedding_model_name in embedding_models: if embedding_model_name in embedding_models:
if embedding_model_name == "text-embedding-ada-002": if embedding_model_name == "text-embedding-ada-002":
embedding_function = embedding_models[embedding_model_name](model=embedding_model_name, api_key=openai_api_key) embedding_function = embedding_models[embedding_model_name](
if embedding_model_name == "all-MiniLM-L6-v2": model=embedding_model_name, api_key=openai_api_key
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name) )
else: else:
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name) raise Exception("Embedding model not found")
# load it into Chroma and use it as a retriever # load it into Chroma and use it as a retriever
db = Chroma.from_documents(texts, embedding_function) db = Chroma.from_documents(texts, embedding_function)
@ -75,7 +73,10 @@ def ask_ai(
memory_key = "history" memory_key = "history"
memory = AgentTokenBufferMemory( memory = AgentTokenBufferMemory(
memory_key=memory_key, llm=llm, chat_memory=message_history, max_token_limit=1000 memory_key=memory_key,
llm=llm,
chat_memory=message_history,
max_token_limit=1000,
) )
system_message = SystemMessage(content=(message_for_the_prompt)) system_message = SystemMessage(content=(message_for_the_prompt))

View file

@ -50,7 +50,8 @@ async def upload_file_and_return_file_object(
await upload_content( await upload_content(
f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}", f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}",
org_uuid=org_uuid, type_of_dir='orgs',
uuid=org_uuid,
file_binary=file_binary, file_binary=file_binary,
file_and_format=f"{file_id}.{file_format}", file_and_format=f"{file_id}.{file_format}",
) )

View file

@ -25,7 +25,7 @@ async def create_activity(
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
): ):
activity = Activity.from_orm(activity_object)
# CHeck if org exists # CHeck if org exists
statement = select(Chapter).where(Chapter.id == activity_object.chapter_id) statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
@ -40,6 +40,9 @@ async def create_activity(
# RBAC check # RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session) await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session)
# Create Activity
activity = Activity(**activity_object.dict())
activity.activity_uuid = str(f"activity_{uuid4()}") activity.activity_uuid = str(f"activity_{uuid4()}")
activity.creation_date = str(datetime.now()) activity.creation_date = str(datetime.now())
activity.update_date = str(datetime.now()) activity.update_date = str(datetime.now())
@ -223,7 +226,6 @@ async def rbac_check(
res = await authorization_verify_if_element_is_public( res = await authorization_verify_if_element_is_public(
request, course_uuid, action, db_session request, course_uuid, action, db_session
) )
print('res',res)
return res return res
else: else:
res = await authorization_verify_based_on_roles_and_authorship( res = await authorization_verify_based_on_roles_and_authorship(

View file

@ -8,6 +8,7 @@ async def upload_pdf(pdf_file, activity_uuid, org_uuid, course_uuid):
try: try:
await upload_content( await upload_content(
f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf", f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
"orgs",
org_uuid, org_uuid,
contents, contents,
f"documentpdf.{pdf_format}", f"documentpdf.{pdf_format}",

View file

@ -9,6 +9,7 @@ async def upload_video(video_file, activity_uuid, org_uuid, course_uuid):
try: try:
await upload_content( await upload_content(
f"courses/{course_uuid}/activities/{activity_uuid}/video", f"courses/{course_uuid}/activities/{activity_uuid}/video",
'orgs',
org_uuid, org_uuid,
contents, contents,
f"video.{video_format}", f"video.{video_format}",

View file

@ -4,6 +4,10 @@ from src.db.courses import CourseRead
def structure_activity_content_by_type(activity): def structure_activity_content_by_type(activity):
### Get Headings, Texts, Callouts, Answers and Paragraphs from the activity as a big list of strings (text only) and return it ### Get Headings, Texts, Callouts, Answers and Paragraphs from the activity as a big list of strings (text only) and return it
if "content" not in activity or not activity["content"]:
return []
content = activity["content"] content = activity["content"]
headings = [] headings = []
@ -11,10 +15,12 @@ def structure_activity_content_by_type(activity):
paragraphs = [] paragraphs = []
for item in content: for item in content:
if 'content' in item: if "content" in item:
if item["type"] == "heading" and "text" in item["content"][0]: if item["type"] == "heading" and "text" in item["content"][0]:
headings.append(item["content"][0]["text"]) headings.append(item["content"][0]["text"])
elif item["type"] in ["calloutInfo", "calloutWarning"] and all("text" in text_item for text_item in item["content"]): elif item["type"] in ["calloutInfo", "calloutWarning"] and all(
"text" in text_item for text_item in item["content"]
):
callouts.append( callouts.append(
"".join([text_item["text"] for text_item in item["content"]]) "".join([text_item["text"] for text_item in item["content"]])
) )
@ -34,15 +40,29 @@ def structure_activity_content_by_type(activity):
# Add Paragraphs # Add Paragraphs
data_array.append({"Paragraphs": paragraphs}) data_array.append({"Paragraphs": paragraphs})
print(data_array)
return data_array return data_array
def serialize_activity_text_to_ai_comprehensible_text( def serialize_activity_text_to_ai_comprehensible_text(
data_array, course: CourseRead, activity: ActivityRead data_array,
course: CourseRead,
activity: ActivityRead,
isActivityEmpty: bool = False,
): ):
### Serialize the text to a format that is comprehensible by the AI
if isActivityEmpty:
text = (
"Use this as a context "
+ 'This is a course about "'
+ course.name
+ '". '
+ 'This is a lecture about "'
+ activity.name
+ '". '
+ "There is no content yet in this lecture."
)
return text
# Serialize Headings # Serialize Headings
serialized_headings = "" serialized_headings = ""
@ -51,7 +71,6 @@ def serialize_activity_text_to_ai_comprehensible_text(
# Serialize Callouts # Serialize Callouts
serialized_callouts = "" serialized_callouts = ""
for callout in data_array[1]["Callouts"]: for callout in data_array[1]["Callouts"]:
serialized_callouts += callout + " " serialized_callouts += callout + " "

View file

@ -225,7 +225,7 @@ async def get_course_chapters(
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session) await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
# Get activities for each chapter # Get activities for each chapter
for chapter in chapters: for chapter in chapters:

View file

@ -26,7 +26,10 @@ from fastapi import HTTPException, status, Request
async def get_collection( async def get_collection(
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session request: Request,
collection_uuid: str,
current_user: PublicUser,
db_session: Session,
) -> CollectionRead: ) -> CollectionRead:
statement = select(Collection).where(Collection.collection_uuid == collection_uuid) statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first() collection = db_session.exec(statement).first()
@ -42,11 +45,23 @@ async def get_collection(
) )
# get courses in collection # get courses in collection
statement = ( statement_all = (
select(Course) select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id) .join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id) .distinct(Course.id)
) )
statement_public = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.where(CollectionCourse.org_id == collection.org_id, Course.public == True)
)
if current_user.id == 0:
statement = statement_public
else:
statement = statement_all
courses = db_session.exec(statement).all() courses = db_session.exec(statement).all()
collection = CollectionRead(**collection.dict(), courses=courses) collection = CollectionRead(**collection.dict(), courses=courses)
@ -180,7 +195,10 @@ async def update_collection(
async def delete_collection( async def delete_collection(
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session request: Request,
collection_uuid: str,
current_user: PublicUser,
db_session: Session,
): ):
statement = select(Collection).where(Collection.collection_uuid == collection_uuid) statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first() collection = db_session.exec(statement).first()
@ -216,23 +234,40 @@ async def get_collections(
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
) -> List[CollectionRead]: ) -> List[CollectionRead]:
# RBAC check
await rbac_check(request, "collection_x", current_user, "read", db_session)
statement = ( statement_public = select(Collection).where(
Collection.org_id == org_id, Collection.public == True
)
statement_all = (
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id) select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
) )
if current_user.id == 0:
statement = statement_public
else:
statement = statement_all
collections = db_session.exec(statement).all() collections = db_session.exec(statement).all()
collections_with_courses = [] collections_with_courses = []
for collection in collections: for collection in collections:
statement = ( statement_all = (
select(Course) select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id) .join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id) .distinct(Course.id)
) )
statement_public = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.where(CollectionCourse.org_id == org_id, Course.public == True)
)
if current_user.id == 0:
statement = statement_public
else:
# RBAC check
statement = statement_all
courses = db_session.exec(statement).all() courses = db_session.exec(statement).all()
collection = CollectionRead(**collection.dict(), courses=courses) collection = CollectionRead(**collection.dict(), courses=courses)
@ -256,8 +291,11 @@ async def rbac_check(
res = await authorization_verify_if_element_is_public( res = await authorization_verify_if_element_is_public(
request, collection_uuid, action, db_session request, collection_uuid, action, db_session
) )
print('res',res) if res == False:
return res raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights : You are not allowed to read this collection",
)
else: else:
res = await authorization_verify_based_on_roles_and_authorship( res = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, collection_uuid, db_session request, current_user.id, action, collection_uuid, db_session
@ -276,4 +314,3 @@ async def rbac_check(
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##

View file

@ -146,6 +146,9 @@ async def create_course(
) )
course.thumbnail_image = name_in_disk course.thumbnail_image = name_in_disk
else:
course.thumbnail_image = ""
# Insert course # Insert course
db_session.add(course) db_session.add(course)
db_session.commit() db_session.commit()

View file

@ -1,13 +1,13 @@
from src.services.utils.upload_content import upload_content from src.services.utils.upload_content import upload_content
async def upload_thumbnail(thumbnail_file, name_in_disk, org_id, course_id): async def upload_thumbnail(thumbnail_file, name_in_disk, org_uuid, course_id):
contents = thumbnail_file.file.read() contents = thumbnail_file.file.read()
try: try:
await upload_content( await upload_content(
f"courses/{course_id}/thumbnails", f"courses/{course_id}/thumbnails",
org_id, "orgs",
org_uuid,
contents, contents,
f"{name_in_disk}", f"{name_in_disk}",
) )

View file

@ -0,0 +1,253 @@
import json
import random
import string
import uuid
import redis
from datetime import datetime, timedelta
from sqlmodel import Session, select
from config.config import get_learnhouse_config
from src.services.orgs.orgs import rbac_check
from src.db.users import AnonymousUser, PublicUser
from src.db.organizations import (
Organization,
)
from fastapi import HTTPException, Request
async def create_invite_code(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Check if this org has more than 6 invite codes
invite_codes = r.keys(f"*:org:{org.org_uuid}:code:*")
if len(invite_codes) >= 6:
raise HTTPException(
status_code=400,
detail="Organization has reached the maximum number of invite codes",
)
# Generate invite code
def generate_code(length=5):
letters_and_digits = string.ascii_letters + string.digits
return "".join(random.choice(letters_and_digits) for _ in range(length))
generated_invite_code = generate_code()
invite_code_uuid = f"org_invite_code_{uuid.uuid4()}"
# time to live in days to seconds
ttl = int(timedelta(days=365).total_seconds())
inviteCodeObject = {
"invite_code": generated_invite_code,
"invite_code_uuid": invite_code_uuid,
"invite_code_expires": ttl,
"invite_code_type": "signup",
"created_at": datetime.now().isoformat(),
"created_by": current_user.user_uuid,
}
r.set(
f"{invite_code_uuid}:org:{org.org_uuid}:code:{generated_invite_code}",
json.dumps(inviteCodeObject),
ex=ttl,
)
return inviteCodeObject
async def get_invite_codes(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Get invite codes
invite_codes = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:*")
invite_codes_list = []
for invite_code in invite_codes:
invite_code = r.get(invite_code)
invite_code = json.loads(invite_code)
invite_codes_list.append(invite_code)
return invite_codes_list
async def get_invite_code(
request: Request,
org_id: int,
invite_code: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
# await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Get invite code
invite_code = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:{invite_code}") # type: ignore
if not invite_code:
raise HTTPException(
status_code=404,
detail="Invite code not found",
)
invite_code = r.get(invite_code[0]) # type: ignore
invite_code = json.loads(invite_code)
return invite_code
async def delete_invite_code(
request: Request,
org_id: int,
invite_code_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Delete invite code
keys = r.keys(f"{invite_code_uuid}:org:{org.org_uuid}:code:*")
if keys:
r.delete(*keys)
if not keys:
raise HTTPException(
status_code=404,
detail="Invite code not found",
)
return keys

View file

@ -9,6 +9,7 @@ async def upload_org_logo(logo_file, org_uuid):
await upload_content( await upload_content(
"logos", "logos",
"orgs",
org_uuid, org_uuid,
contents, contents,
name_in_disk, name_in_disk,

View file

@ -15,7 +15,7 @@ from src.db.organization_config import (
OrganizationConfigBase, OrganizationConfigBase,
) )
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_org_admin_status,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
@ -169,7 +169,7 @@ async def create_org(
limits_enabled=False, limits_enabled=False,
max_asks=0, max_asks=0,
), ),
embeddings="all-MiniLM-L6-v2", embeddings="text-embedding-ada-002",
ai_model="gpt-3.5-turbo", ai_model="gpt-3.5-turbo",
features=AIEnabledFeatures( features=AIEnabledFeatures(
editor=False, editor=False,
@ -438,12 +438,106 @@ async def get_orgs_by_user(
return orgs return orgs
# Config related
async def update_org_signup_mechanism(
request: Request,
signup_mechanism: Literal["open", "inviteOnly"],
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Get org config
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
result = db_session.exec(statement)
org_config = result.first()
if org_config is None:
logging.error(f"Organization {org_id} has no config")
raise HTTPException(
status_code=404,
detail="Organization config not found",
)
updated_config = org_config.config
# Update config
updated_config = OrganizationConfigBase(**updated_config)
updated_config.GeneralConfig.users.signup_mechanism = signup_mechanism
# Update the database
org_config.config = json.loads(updated_config.json())
org_config.update_date = str(datetime.now())
db_session.add(org_config)
db_session.commit()
db_session.refresh(org_config)
return {"detail": "Signup mechanism updated"}
async def get_org_join_mechanism(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
# Get org config
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
result = db_session.exec(statement)
org_config = result.first()
if org_config is None:
logging.error(f"Organization {org_id} has no config")
raise HTTPException(
status_code=404,
detail="Organization config not found",
)
config = org_config.config
# Get the signup mechanism
config = OrganizationConfigBase(**config)
signup_mechanism = config.GeneralConfig.users.signup_mechanism
return signup_mechanism
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##
async def rbac_check( async def rbac_check(
request: Request, request: Request,
org_id: str, org_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session, db_session: Session,
@ -453,11 +547,25 @@ async def rbac_check(
return True return True
else: else:
await authorization_verify_if_user_is_anon(current_user.id) isUserAnon = await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship( isAllowedOnOrgAdminStatus = (
request, current_user.id, action, org_id, db_session await authorization_verify_based_on_org_admin_status(
request, current_user.id, action, org_uuid, db_session
)
) )
if isUserAnon:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You should be logged in to be able to achieve this action",
)
if not isAllowedOnOrgAdminStatus:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (admin status) : You don't have the right to perform this action",
)
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##

View file

@ -0,0 +1,410 @@
from datetime import datetime, timedelta
import json
import logging
import redis
from fastapi import HTTPException, Request
from sqlmodel import Session, select
from config.config import get_learnhouse_config
from src.services.orgs.orgs import rbac_check
from src.db.roles import Role, RoleRead
from src.db.users import AnonymousUser, PublicUser, User, UserRead
from src.db.user_organizations import UserOrganization
from src.db.organizations import (
Organization,
OrganizationUser,
)
async def get_organization_users(
request: Request,
org_id: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
) -> list[OrganizationUser]:
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
statement = (
select(User)
.join(UserOrganization)
.join(Organization)
.where(Organization.id == org_id)
)
users = db_session.exec(statement)
users = users.all()
org_users_list = []
for user in users:
statement = select(UserOrganization).where(
UserOrganization.user_id == user.id, UserOrganization.org_id == org_id
)
result = db_session.exec(statement)
user_org = result.first()
if not user_org:
logging.error(f"User {user.id} not found")
# skip this user
continue
statement = select(Role).where(Role.id == user_org.role_id)
result = db_session.exec(statement)
role = result.first()
if not role:
logging.error(f"Role {user_org.role_id} not found")
# skip this user
continue
statement = select(User).where(User.id == user_org.user_id)
result = db_session.exec(statement)
user = result.first()
if not user:
logging.error(f"User {user_org.user_id} not found")
# skip this user
continue
user = UserRead.from_orm(user)
role = RoleRead.from_orm(role)
org_user = OrganizationUser(
user=user,
role=role,
)
org_users_list.append(org_user)
return org_users_list
async def remove_user_from_org(
request: Request,
org_id: int,
user_id: int,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
statement = select(UserOrganization).where(
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
)
result = db_session.exec(statement)
user_org = result.first()
if not user_org:
raise HTTPException(
status_code=404,
detail="User not found",
)
# Check if user is the last admin
statement = select(UserOrganization).where(
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
)
result = db_session.exec(statement)
admins = result.all()
if len(admins) == 1 and admins[0].user_id == user_id:
raise HTTPException(
status_code=400,
detail="You can't remove the last admin of the organization",
)
db_session.delete(user_org)
db_session.commit()
return {"detail": "User removed from org"}
async def update_user_role(
request: Request,
org_id: str,
user_id: str,
role_uuid: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
# find role
statement = select(Role).where(Role.role_uuid == role_uuid)
result = db_session.exec(statement)
role = result.first()
if not role:
raise HTTPException(
status_code=404,
detail="Role not found",
)
role_id = role.id
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Check if user is the last admin and if the new role is not admin
statement = select(UserOrganization).where(
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
)
result = db_session.exec(statement)
admins = result.all()
if not admins:
raise HTTPException(
status_code=400,
detail="There is no admin in the organization",
)
if (
len(admins) == 1
and int(admins[0].user_id) == int(user_id)
and str(role_uuid) != "role_global_admin"
):
raise HTTPException(
status_code=400,
detail="Organization must have at least one admin",
)
statement = select(UserOrganization).where(
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
)
result = db_session.exec(statement)
user_org = result.first()
if not user_org:
raise HTTPException(
status_code=404,
detail="User not found",
)
if role_id is not None:
user_org.role_id = role_id
db_session.add(user_org)
db_session.commit()
db_session.refresh(user_org)
return {"detail": "User role updated"}
async def invite_batch_users(
request: Request,
org_id: int,
emails: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
invite_list = emails.split(",")
# invitations expire after 30 days
ttl = int(timedelta(days=365).total_seconds())
for email in invite_list:
email = email.strip()
# Check if user is already invited
invited_user = r.get(f"invited_user:{email}:org:{org.org_uuid}")
if invited_user:
logging.error(f"User {email} already invited")
# skip this user
continue
invited_user_object = {
"email": email,
"org_id": org.id,
"pending": True,
"email_sent": False,
"expires": ttl,
"created_at": datetime.now().isoformat(),
"created_by": current_user.user_uuid,
}
invited_user = r.set(
f"invited_user:{email}:org:{org.org_uuid}",
json.dumps(invited_user_object),
ex=ttl,
)
return {"detail": "Users invited"}
async def get_list_of_invited_users(
request: Request,
org_id: int,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
invited_users = r.keys(f"invited_user:*:org:{org.org_uuid}")
invited_users_list = []
for user in invited_users:
invited_user = r.get(user)
if invited_user:
invited_user = json.loads(invited_user.decode("utf-8"))
invited_users_list.append(invited_user)
return invited_users_list
async def remove_invited_user(
request: Request,
org_id: int,
email: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
invited_user = r.get(f"invited_user:{email}:org:{org.org_uuid}")
if not invited_user:
raise HTTPException(
status_code=404,
detail="User not found",
)
r.delete(f"invited_user:{email}:org:{org.org_uuid}")
return {"detail": "User removed"}

View file

@ -17,7 +17,9 @@ async def create_user_trail(
trail_object: TrailCreate, trail_object: TrailCreate,
db_session: Session, db_session: Session,
) -> Trail: ) -> Trail:
statement = select(Trail).where(Trail.org_id == trail_object.org_id, Trail.user_id == user.id) statement = select(Trail).where(
Trail.org_id == trail_object.org_id, Trail.user_id == user.id
)
trail = db_session.exec(statement).first() trail = db_session.exec(statement).first()
if trail: if trail:
@ -124,7 +126,7 @@ async def check_trail_presence(
async def get_user_trail_with_orgid( async def get_user_trail_with_orgid(
request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session
) -> TrailRead: ) -> TrailRead:
if isinstance(user, AnonymousUser): if isinstance(user, AnonymousUser):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -151,7 +153,7 @@ async def get_user_trail_with_orgid(
for trail_run in trail_runs: for trail_run in trail_runs:
statement = select(Course).where(Course.id == trail_run.course_id) statement = select(Course).where(Course.id == trail_run.course_id)
course = db_session.exec(statement).first() course = db_session.exec(statement).first()
trail_run.course = course trail_run.course = course
# Add number of activities (steps) in a course # Add number of activities (steps) in a course
statement = select(ChapterActivity).where( statement = select(ChapterActivity).where(
@ -213,7 +215,7 @@ async def add_activity_to_trail(
) )
statement = select(TrailRun).where( statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id TrailRun.trail_id == trail.id, TrailRun.course_id == course.id, TrailRun.user_id == user.id
) )
trailrun = db_session.exec(statement).first() trailrun = db_session.exec(statement).first()
@ -231,7 +233,7 @@ async def add_activity_to_trail(
db_session.refresh(trailrun) db_session.refresh(trailrun)
statement = select(TrailStep).where( statement = select(TrailStep).where(
TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id, TrailStep.user_id == user.id
) )
trailstep = db_session.exec(statement).first() trailstep = db_session.exec(statement).first()
@ -253,7 +255,7 @@ async def add_activity_to_trail(
db_session.commit() db_session.commit()
db_session.refresh(trailstep) db_session.refresh(trailstep)
statement = select(TrailRun).where(TrailRun.trail_id == trail.id) statement = select(TrailRun).where(TrailRun.trail_id == trail.id , TrailRun.user_id == user.id)
trail_runs = db_session.exec(statement).all() trail_runs = db_session.exec(statement).all()
trail_runs = [ trail_runs = [
@ -262,7 +264,7 @@ async def add_activity_to_trail(
] ]
for trail_run in trail_runs: for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id, TrailStep.user_id == user.id)
trail_steps = db_session.exec(statement).all() trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
@ -296,7 +298,9 @@ async def add_course_to_trail(
) )
# check if run already exists # check if run already exists
statement = select(TrailRun).where(TrailRun.course_id == course.id) statement = select(TrailRun).where(
TrailRun.course_id == course.id, TrailRun.user_id == user.id
)
trailrun = db_session.exec(statement).first() trailrun = db_session.exec(statement).first()
if trailrun: if trailrun:
@ -315,7 +319,7 @@ async def add_course_to_trail(
) )
statement = select(TrailRun).where( statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id TrailRun.trail_id == trail.id, TrailRun.course_id == course.id, TrailRun.user_id == user.id
) )
trail_run = db_session.exec(statement).first() trail_run = db_session.exec(statement).first()
@ -332,7 +336,7 @@ async def add_course_to_trail(
db_session.commit() db_session.commit()
db_session.refresh(trail_run) db_session.refresh(trail_run)
statement = select(TrailRun).where(TrailRun.trail_id == trail.id) statement = select(TrailRun).where(TrailRun.trail_id == trail.id, TrailRun.user_id == user.id)
trail_runs = db_session.exec(statement).all() trail_runs = db_session.exec(statement).all()
trail_runs = [ trail_runs = [
@ -341,7 +345,7 @@ async def add_course_to_trail(
] ]
for trail_run in trail_runs: for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id , TrailStep.user_id == user.id)
trail_steps = db_session.exec(statement).all() trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
@ -385,7 +389,7 @@ async def remove_course_from_trail(
) )
statement = select(TrailRun).where( statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id TrailRun.trail_id == trail.id, TrailRun.course_id == course.id, TrailRun.user_id == user.id
) )
trail_run = db_session.exec(statement).first() trail_run = db_session.exec(statement).first()
@ -394,14 +398,14 @@ async def remove_course_from_trail(
db_session.commit() db_session.commit()
# Delete all trail steps for this course # Delete all trail steps for this course
statement = select(TrailStep).where(TrailStep.course_id == course.id) statement = select(TrailStep).where(TrailStep.course_id == course.id, TrailStep.user_id == user.id)
trail_steps = db_session.exec(statement).all() trail_steps = db_session.exec(statement).all()
for trail_step in trail_steps: for trail_step in trail_steps:
db_session.delete(trail_step) db_session.delete(trail_step)
db_session.commit() db_session.commit()
statement = select(TrailRun).where(TrailRun.trail_id == trail.id) statement = select(TrailRun).where(TrailRun.trail_id == trail.id, TrailRun.user_id == user.id)
trail_runs = db_session.exec(statement).all() trail_runs = db_session.exec(statement).all()
trail_runs = [ trail_runs = [
@ -410,7 +414,7 @@ async def remove_course_from_trail(
] ]
for trail_run in trail_runs: for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id, TrailStep.user_id == user.id)
trail_steps = db_session.exec(statement).all() trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]

View file

@ -0,0 +1,16 @@
from src.services.utils.upload_content import upload_content
async def upload_avatar(avatar_file, name_in_disk, user_uuid):
contents = avatar_file.file.read()
try:
await upload_content(
"avatars",
"users",
user_uuid,
contents,
f"{name_in_disk}",
)
except Exception:
return {"message": "There was an error uploading the file"}

View file

@ -1,13 +1,15 @@
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, UploadFile, status
from sqlmodel import Session, select from sqlmodel import Session, select
from src.services.orgs.invites import get_invite_code
from src.services.users.avatars import upload_avatar
from src.db.roles import Role, RoleRead from src.db.roles import Role, RoleRead
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.organizations import Organization, OrganizationRead from src.db.organizations import Organization, OrganizationRead
from src.db.users import ( from src.db.users import (
AnonymousUser, AnonymousUser,
@ -102,6 +104,27 @@ async def create_user(
return user return user
async def create_user_with_invite(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_object: UserCreate,
org_id: int,
invite_code: str,
):
# Check if invite code exists
isInviteCodeCorrect = await get_invite_code(request, org_id, invite_code, current_user, db_session)
if not isInviteCodeCorrect:
raise HTTPException(
status_code=400,
detail="Invite code is incorrect",
)
user = await create_user(request, db_session, current_user, user_object, org_id)
return user
async def create_user_without_org( async def create_user_without_org(
request: Request, request: Request,
@ -195,6 +218,49 @@ async def update_user(
return user return user
async def update_user_avatar(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
avatar_file: UploadFile | None = None,
):
# Get user
statement = select(User).where(User.id == current_user.id)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
# Upload thumbnail
if avatar_file and avatar_file.filename:
name_in_disk = f"{user.user_uuid}_avatar_{uuid4()}.{avatar_file.filename.split('.')[-1]}"
await upload_avatar(avatar_file, name_in_disk, user.user_uuid)
# Update course
if name_in_disk:
user.avatar_image = name_in_disk
else:
raise HTTPException(
status_code=500,
detail="Issue with Avatar upload",
)
# Update user in database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
user = UserRead.from_orm(user)
return user
async def update_user_password( async def update_user_password(
request: Request, request: Request,
db_session: Session, db_session: Session,

View file

@ -1,3 +1,4 @@
from typing import Literal
import boto3 import boto3
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
import os import os
@ -6,7 +7,11 @@ from config.config import get_learnhouse_config
async def upload_content( async def upload_content(
directory: str, org_uuid: str, file_binary: bytes, file_and_format: str directory: str,
type_of_dir: Literal["orgs", "users"],
uuid: str, # org_uuid or user_uuid
file_binary: bytes,
file_and_format: str,
): ):
# Get Learnhouse Config # Get Learnhouse Config
learnhouse_config = get_learnhouse_config() learnhouse_config = get_learnhouse_config()
@ -16,12 +21,12 @@ async def upload_content(
if content_delivery == "filesystem": if content_delivery == "filesystem":
# create folder for activity # create folder for activity
if not os.path.exists(f"content/{org_uuid}/{directory}"): if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):
# create folder for activity # create folder for activity
os.makedirs(f"content/{org_uuid}/{directory}") os.makedirs(f"content/{type_of_dir}/{uuid}/{directory}")
# upload file to server # upload file to server
with open( with open(
f"content/{org_uuid}/{directory}/{file_and_format}", f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
"wb", "wb",
) as f: ) as f:
f.write(file_binary) f.write(file_binary)
@ -37,13 +42,13 @@ async def upload_content(
) )
# Create folder for activity # Create folder for activity
if not os.path.exists(f"content/{org_uuid}/{directory}"): if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):
# create folder for activity # create folder for activity
os.makedirs(f"content/{org_uuid}/{directory}") os.makedirs(f"content/{type_of_dir}/{uuid}/{directory}")
# Upload file to server # Upload file to server
with open( with open(
f"content/{org_uuid}/{directory}/{file_and_format}", f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
"wb", "wb",
) as f: ) as f:
f.write(file_binary) f.write(file_binary)
@ -52,9 +57,9 @@ async def upload_content(
print("Uploading to s3 using boto3...") print("Uploading to s3 using boto3...")
try: try:
s3.upload_file( s3.upload_file(
f"content/{org_uuid}/{directory}/{file_and_format}", f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
"learnhouse-media", "learnhouse-media",
f"content/{org_uuid}/{directory}/{file_and_format}", f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
) )
except ClientError as e: except ClientError as e:
print(e) print(e)
@ -63,7 +68,7 @@ async def upload_content(
try: try:
s3.head_object( s3.head_object(
Bucket="learnhouse-media", Bucket="learnhouse-media",
Key=f"content/{org_uuid}/{directory}/{file_and_format}", Key=f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
) )
print("File upload successful!") print("File upload successful!")
except Exception as e: except Exception as e:

View file

@ -1,13 +1,24 @@
'use client' 'use client'
import React, { use, useEffect } from 'react' import React, { useEffect } from 'react'
import { INSTALL_STEPS } from './steps/steps' import { INSTALL_STEPS } from './steps/steps'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper' import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
function InstallClient() { function InstallClient() {
return (
<GeneralWrapperStyled>
<Suspense>
<>
<Stepscomp />
</>
</Suspense>
</GeneralWrapperStyled>
)
}
const Stepscomp = () => {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const step: any = parseInt(searchParams.get('step') || '0'); const step: any = parseInt(searchParams.get('step') || '0');
@ -24,7 +35,7 @@ function InstallClient() {
}, [step]) }, [step])
return ( return (
<GeneralWrapperStyled> <div>
<div className='flex justify-center '> <div className='flex justify-center '>
<div className='grow'> <div className='grow'>
<LearnHouseLogo /> <LearnHouseLogo />
@ -54,7 +65,7 @@ function InstallClient() {
{stepsState[stepNumber].component} {stepsState[stepNumber].component}
</div> </div>
</div> </div>
</GeneralWrapperStyled> </div>
) )
} }

View file

@ -1,29 +1,27 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import React from "react"; import React, { useState } from "react";
import { createCollection } from "@services/courses/collections"; import { createCollection } from "@services/courses/collections";
import useSWR from "swr"; import useSWR from "swr";
import { getAPIUrl, getUriWithOrg } from "@services/config/config"; import { getAPIUrl, getUriWithOrg } from "@services/config/config";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { getOrganizationContextInfo } from "@services/organizations/orgs"; import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { useOrg } from "@components/Contexts/OrgContext";
function NewCollection(params: any) { function NewCollection(params: any) {
const org = useOrg() as any;
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const [name, setName] = React.useState(""); const [name, setName] = React.useState("");
const [org, setOrg] = React.useState({}) as any;
const [description, setDescription] = React.useState(""); const [description, setDescription] = React.useState("");
const [selectedCourses, setSelectedCourses] = React.useState([]) as any; const [selectedCourses, setSelectedCourses] = React.useState([]) as any;
const router = useRouter(); const router = useRouter();
const { data: courses, error: error } = useSWR(`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`, swrFetcher); const { data: courses, error: error } = useSWR(`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`, swrFetcher);
const [isPublic, setIsPublic] = useState('true');
const handleVisibilityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setIsPublic(e.target.value);
};
React.useEffect(() => {
async function getOrg() {
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800 });
setOrg(org);
}
getOrg();
}, []);
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value); setName(event.target.value);
@ -35,83 +33,94 @@ function NewCollection(params: any) {
const handleSubmit = async (e: any) => { const handleSubmit = async (e: any) => {
e.preventDefault(); e.preventDefault();
const collection = { const collection = {
name: name, name: name,
description: description, description: description,
courses: selectedCourses, courses: selectedCourses,
public: true, public: isPublic,
org_id: org.id, org_id: org.id,
}; };
await createCollection(collection); await createCollection(collection);
await revalidateTags(["collections"], orgslug); await revalidateTags(["collections"], org.slug);
// reload the page
router.refresh(); router.refresh();
router.prefetch(getUriWithOrg(orgslug, "/collections"));
router.push(getUriWithOrg(orgslug, "/collections")); // wait for 2s before reloading the page
setTimeout(() => {
router.push(getUriWithOrg(orgslug, "/collections"));
}
, 1000);
}; };
return ( return (
<> <>
<div className="w-64 m-auto py-20"> <div className="w-64 m-auto py-20">
<div className="font-bold text-lg mb-4">Add new</div> <div className="font-bold text-lg mb-4">Add new</div>
<input <input
type="text" type="text"
placeholder="Name" placeholder="Name"
value={name} value={name}
onChange={handleNameChange} onChange={handleNameChange}
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
{!courses ? ( <select
onChange={handleVisibilityChange}
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
defaultValue={isPublic}
>
<option value="false">Private Collection</option>
<option value="true">Public Collection </option>
</select>
{!courses ? (
<p className="text-gray-500">Loading...</p> <p className="text-gray-500">Loading...</p>
) : ( ) : (
<div> <div className="space-y-4 p-3">
<p>Courses</p>
{courses.map((course: any) => ( {courses.map((course: any) => (
<div key={course.course_uuid} className="flex items-center mb-2"> <div key={course.course_uuid} className="flex items-center space-x-2">
<input <input
type="checkbox"
id={course.id}
name={course.name}
value={course.id}
onChange={(e) => {
if (e.target.checked) {
setSelectedCourses([...selectedCourses, course.id]);
}
else {
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
}
}}
className="text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
type="checkbox" <label htmlFor={course.course_uuid} className="text-sm text-gray-700">{course.name}</label>
id={course.id}
name={course.name}
value={course.id}
// id is an integer, not a string
onChange={(e) => {
if (e.target.checked) {
setSelectedCourses([...selectedCourses, course.id]);
}
else {
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
}
}
}
className="mr-2"
/>
<label htmlFor={course.course_uuid} className="text-sm">{course.name}</label>
</div> </div>
))} ))}
</div> </div>
)} )}
<input <input
type="text" type="text"
placeholder="Description" placeholder="Description"
value={description} value={description}
onChange={handleDescriptionChange} onChange={handleDescriptionChange}
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
<button <button
onClick={handleSubmit} onClick={handleSubmit}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500" className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
Submit Submit
</button> </button>
</div> </div>
</> </>

View file

@ -8,11 +8,12 @@ import { revalidateTags } from "@services/utils/ts/requests";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators"; import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getCourseThumbnailMediaDirectory } from "@services/media/media"; import { getCourseThumbnailMediaDirectory, getUserAvatarMediaDirectory } from "@services/media/media";
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react"; import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
import Avvvatars from "avvvatars-react"; import Avvvatars from "avvvatars-react";
import { getUser } from "@services/users/users"; import { getUser } from "@services/users/users";
import { useOrg } from "@components/Contexts/OrgContext"; import { useOrg } from "@components/Contexts/OrgContext";
import UserAvatar from "@components/Objects/UserAvatar";
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
@ -25,7 +26,7 @@ const CourseClient = (props: any) => {
function getLearningTags() { function getLearningTags() {
// create array of learnings from a string object (comma separated) // create array of learnings from a string object (comma separated)
let learnings = course.learnings.split(","); let learnings = course?.learnings ? course?.learnings.split(",") : [];
setLearnings(learnings); setLearnings(learnings);
} }
@ -56,13 +57,13 @@ const CourseClient = (props: any) => {
} }
useEffect(() => { useEffect(() => {
getLearningTags();
} }
, [org]); , [org, course]);
return ( return (
<> <>
{!course ? ( {!course && !org ? (
<PageLoading></PageLoading> <PageLoading></PageLoading>
) : ( ) : (
<GeneralWrapperStyled> <GeneralWrapperStyled>
@ -73,9 +74,13 @@ const CourseClient = (props: any) => {
</h1> </h1>
</div> </div>
{props.course?.thumbnail_image && org ?
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course?.course_uuid, course?.thumbnail_image)})` }}>
</div> </div>
:
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" style={{ backgroundImage: `url('../empty_thumbnail.png')`, backgroundSize: 'auto' }}>
</div>
}
<ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} /> <ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
@ -86,21 +91,25 @@ const CourseClient = (props: any) => {
<p className="py-5 px-5">{course.description}</p> <p className="py-5 px-5">{course.description}</p>
</div> </div>
<h2 className="py-3 text-2xl font-bold">What you will learn</h2> {learnings.length > 0 && learnings[0] !== "null" &&
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2"> <div>
{learnings.map((learning: any) => { <h2 className="py-3 text-2xl font-bold">What you will learn</h2>
return ( <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
<div key={learning} {learnings.map((learning: any) => {
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize"> return (
<div className="px-2 py-2 rounded-full"> <div key={learning}
<Check className="text-gray-400" size={15} /> className="flex space-x-2 items-center font-semibold text-gray-500">
</div> <div className="px-2 py-2 rounded-full">
<p>{learning}</p> <Check className="text-gray-400" size={15} />
</div> </div>
); <p>{learning}</p>
} </div>
)} );
</div> }
)}
</div>
</div>
}
<h2 className="py-3 text-2xl font-bold">Course Lessons</h2> <h2 className="py-3 text-2xl font-bold">Course Lessons</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
@ -185,15 +194,22 @@ const CourseClient = (props: any) => {
</div> </div>
</div> </div>
<div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="course_metadata_right space-y-3 w-72 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
{user && {user &&
<div className="flex mx-auto space-x-3 px-2 py-2 items-center"> <div className="flex flex-col mx-auto space-y-3 px-2 py-2 items-center">
<div className=""> <UserAvatar border="border-8" avatar_url={getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image)} width={100} />
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.authors[0].username} style='shape' />
</div>
<div className="-space-y-2 "> <div className="-space-y-2 ">
<div className="text-[12px] text-neutral-400 font-semibold">Author</div> <div className="text-[12px] text-neutral-400 font-semibold">Author</div>
<div className="text-xl font-bold text-neutral-800">{course.authors[0].first_name} {course.authors[0].last_name} {(course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username}</div> <div className="text-xl font-bold text-neutral-800">
{course.authors[0].first_name && course.authors[0].last_name && (
<div className="flex space-x-2 items-center">
<p>{course.authors[0].first_name + ' ' + course.authors[0].last_name}</p><span className="text-xs bg-neutral-100 p-1 px-3 rounded-full text-neutral-400 font-semibold"> @{course.authors[0].username}</span>
</div>)}
{!course.authors[0].first_name && !course.authors[0].last_name && (
<div className="flex space-x-2 items-center">
<p>@{course.authors[0].username}</p>
</div>)}
</div>
</div> </div>
</div> </div>
} }
@ -214,12 +230,4 @@ const CourseClient = (props: any) => {
}; };
const StyledBox = (props: any) => (
<div className="p-3 pl-10 bg-white w-[100%] h-auto ring-1 ring-inset ring-gray-400/10 rounded-lg shadow-sm">
{props.children}
</div>
);
export default CourseClient; export default CourseClient;

View file

@ -17,7 +17,7 @@ export default function Error({
return ( return (
<div> <div>
<ErrorUI></ErrorUI> <ErrorUI ></ErrorUI>
</div> </div>
); );
} }

View file

@ -32,9 +32,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
} }
return ( return (
<div className='h-full w-full bg-[#f8f8f8]'> <div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}> <CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'> <div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
<CourseOverviewTop params={params} /> <CourseOverviewTop params={params} />
<div className='flex space-x-5 font-black text-sm'> <div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}> <Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
@ -57,12 +57,12 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
</Link> </Link>
</div> </div>
</div> </div>
<div className='h-6'></div>
<motion.div <motion.div
initial={{ opacity: 0, }} initial={{ opacity: 0, }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }} transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
className='h-full overflow-y-auto'
> >
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''} {params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''} {params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}

View file

@ -1,17 +1,20 @@
import SessionProvider from '@components/Contexts/SessionContext' import SessionProvider from '@components/Contexts/SessionContext'
import LeftMenu from '@components/Dashboard/UI/LeftMenu' import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import React from 'react' import React from 'react'
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) { function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
return ( return (
<> <>
<SessionProvider> <SessionProvider>
<div className='flex'> <AdminAuthorization authorizationMode="page">
<LeftMenu/> <div className='flex'>
<div className='flex w-full'> <LeftMenu />
{children} <div className='flex w-full'>
</div> {children}
</div> </div>
</div>
</AdminAuthorization>
</SessionProvider> </SessionProvider>
</> </>
) )

View file

@ -1,11 +1,63 @@
import PageLoading from '@components/Objects/Loaders/PageLoading' import Image from 'next/image'
import React from 'react' import React from 'react'
import learnhousetextlogo from '../../../../public/learnhouse_logo.png'
import learnhouseiconlogo from '../../../../public/learnhouse_bigicon.png'
import { BookCopy, School, Settings, Users } from 'lucide-react'
import Link from 'next/link'
import AdminAuthorization from '@components/Security/AdminAuthorization'
function DashboardHome() { function DashboardHome() {
return ( return (
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3"> <div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
<PageLoading /> <div className='mx-auto pb-10'>
<div className='text-neutral-400 font-bold animate-pulse text-2xl'>This page is work in progress</div> <Image alt='learnhouse logo' width={230} src={learnhousetextlogo}></Image>
</div>
<AdminAuthorization authorizationMode="component">
<div className='flex space-x-10'>
<Link href={`/dash/courses`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
<div className='flex flex-col mx-auto space-y-2'>
<BookCopy className='mx-auto text-gray-500' size={50}></BookCopy>
<div className='text-center font-bold text-gray-500'>Courses</div>
<p className='text-center text-sm text-gray-400'>Create and manage courses, chapters and ativities </p>
</div>
</Link>
<Link href={`/dash/org/settings/general`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
<div className='flex flex-col mx-auto space-y-2'>
<School className='mx-auto text-gray-500' size={50}></School>
<div className='text-center font-bold text-gray-500'>Organization</div>
<p className='text-center text-sm text-gray-400'>Configure your Organization general settings </p>
</div>
</Link>
<Link href={`/dash/users/settings/users`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
<div className='flex flex-col mx-auto space-y-2'>
<Users className='mx-auto text-gray-500' size={50}></Users>
<div className='text-center font-bold text-gray-500'>Users</div>
<p className='text-center text-sm text-gray-400'>Manage your Organization's users, roles </p>
</div>
</Link>
</div>
</AdminAuthorization>
<div className='flex flex-col space-y-10 '>
<AdminAuthorization authorizationMode="component">
<div className='h-1 w-[100px] bg-neutral-200 rounded-full mx-auto'></div>
<div className="flex justify-center items-center">
<Link href={'https://learn.learnhouse.io/'} className='flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer'>
<BookCopy className=' text-gray-100' size={20}></BookCopy>
<div className=' text-sm font-bold text-gray-100'>Learn LearnHouse</div>
</Link>
</div>
<div className='mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full'></div>
</AdminAuthorization>
<Link href={'/dash/user-account/settings/general'} className='flex bg-white shadow-lg p-[15px] items-center rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
<div className='flex flex-row mx-auto space-x-3 items-center'>
<Settings className=' text-gray-500' size={20}></Settings>
<div className=' font-bold text-gray-500'>Account Settings</div>
<p className=' text-sm text-gray-400'>Configure your personal settings, passwords, email</p>
</div>
</Link>
</div>
</div> </div>
) )
} }

View file

@ -1,8 +1,8 @@
'use client'; 'use client';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import UserEditGeneral from '@components/Dashboard/User/UserEditGeneral/UserEditGeneral'; import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
import UserEditPassword from '@components/Dashboard/User/UserEditPassword/UserEditPassword'; import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
import Link from 'next/link'; import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config'; import { getUriWithOrg } from '@services/config/config';
import { Info, Lock } from 'lucide-react'; import { Info, Lock } from 'lucide-react';
@ -24,7 +24,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
return ( return (
<div className='h-full w-full bg-[#f8f8f8]'> <div className='h-full w-full bg-[#f8f8f8]'>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'> <div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs> <BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
<div className='my-2 tracking-tighter'> <div className='my-2 tracking-tighter'>
<div className='w-100 flex justify-between'> <div className='w-100 flex justify-between'>
@ -32,7 +32,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
</div> </div>
</div> </div>
<div className='flex space-x-5 font-black text-sm'> <div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/general`}> <Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}> <div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'> <div className='flex items-center space-x-2.5 mx-2'>
@ -41,7 +41,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
</div> </div>
</div> </div>
</Link> </Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/security`}> <Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/security`}>
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}> <div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'> <div className='flex items-center space-x-2.5 mx-2'>
<Lock size={16} /> <Lock size={16} />
@ -58,6 +58,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }} transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
className='h-full overflow-y-auto'
> >
{params.subpage == 'general' ? <UserEditGeneral /> : ''} {params.subpage == 'general' ? <UserEditGeneral /> : ''}
{params.subpage == 'security' ? <UserEditPassword /> : ''} {params.subpage == 'security' ? <UserEditPassword /> : ''}

View file

@ -0,0 +1,103 @@
'use client';
import React, { useEffect } from 'react'
import { motion } from 'framer-motion';
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config';
import { Info, Lock, ScanEye, User, UserCog, UserPlus, Users } from 'lucide-react';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
import { useSession } from '@components/Contexts/SessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers';
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess';
export type SettingsParams = {
subpage: string
orgslug: string
}
function UsersSettingsPage({ params }: { params: SettingsParams }) {
const session = useSession() as any;
const org = useOrg() as any;
const [H1Label, setH1Label] = React.useState('')
const [H2Label, setH2Label] = React.useState('')
function handleLabels() {
if (params.subpage == 'users') {
setH1Label('Users')
setH2Label('Manage your organization users, assign roles and permissions')
}
if (params.subpage == 'signups') {
setH1Label('Signup Access')
setH2Label('Choose from where users can join your organization')
}
if (params.subpage == 'add') {
setH1Label('Invite users')
setH2Label('Invite users to join your organization')
}
}
useEffect(() => {
handleLabels()
}
, [session, org, params.subpage, params])
return (
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
<BreadCrumbs type='orgusers' ></BreadCrumbs>
<div className='my-2 py-3'>
<div className='w-100 flex flex-col space-y-1'>
<div className='pt-3 flex font-bold text-4xl tracking-tighter'>{H1Label}</div>
<div className='flex font-medium text-gray-400 text-md'>{H2Label} </div>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/users`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'users' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Users size={16} />
<div>Users</div>
</div>
</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/add`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'add' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<UserPlus size={16} />
<div>Invite users</div>
</div>
</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/signups`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'signups' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<ScanEye size={16} />
<div>Signup Access</div>
</div>
</div>
</Link>
</div>
</div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
className='h-full overflow-y-auto'
>
{params.subpage == 'users' ? <OrgUsers /> : ''}
{params.subpage == 'signups' ? <OrgAccess /> : ''}
</motion.div>
</div>
)
}
export default UsersSettingsPage

View file

@ -1,11 +1,10 @@
"use client"; "use client";;
import learnhouseIcon from "public/learnhouse_bigicon_1.png"; import learnhouseIcon from "public/learnhouse_bigicon_1.png";
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form' import FormLayout, { FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form';
import Image from 'next/image'; import Image from 'next/image';
import * as Form from '@radix-ui/react-form'; import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { getOrgLogoMediaDirectory } from "@services/media/media"; import { getOrgLogoMediaDirectory } from "@services/media/media";
import { BarLoader } from "react-spinners";
import React from "react"; import React from "react";
import { loginAndGetToken } from "@services/auth/auth"; import { loginAndGetToken } from "@services/auth/auth";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
@ -79,14 +78,14 @@ const LoginClient = (props: LoginClientProps) => {
<div className="m-auto flex space-x-4 items-center flex-wrap"> <div className="m-auto flex space-x-4 items-center flex-wrap">
<div>Login to </div> <div>Login to </div>
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" > <div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
{props.org?.logo ? ( {props.org?.logo_image ? (
<img <img
src={`${getOrgLogoMediaDirectory(props.org.org_id, props.org?.logo)}`} src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
alt="Learnhouse" alt="Learnhouse"
style={{ width: "auto", height: 70 }} style={{ width: "auto", height: 70 }}
className="rounded-md shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white" className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
/> />
) : ( ) : (
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" /> <Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
)} )}
</div> </div>

View file

@ -12,7 +12,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const orgslug = params.orgslug; const orgslug = params.orgslug;
// Get Org context information // Get Org context information
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
return { return {
title: 'Login' + `${org.name}`, title: 'Login' + `${org.name}`,
@ -21,7 +21,7 @@ export async function generateMetadata(
const Login = async (params: any) => { const Login = async (params: any) => {
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
return ( return (
<div> <div>

View file

@ -0,0 +1,166 @@
"use client";
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import React, { useEffect } from 'react'
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
import * as Form from '@radix-ui/react-form';
import { AlertTriangle, Check, User } from 'lucide-react';
import Link from 'next/link';
import { signUpWithInviteCode } from '@services/auth/auth';
import { useOrg } from '@components/Contexts/OrgContext';
const validate = (values: any) => {
const errors: any = {};
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.username) {
errors.username = 'Required';
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters';
}
if (!values.bio) {
errors.bio = 'Required';
}
return errors;
};
interface InviteOnlySignUpProps {
inviteCode: string;
}
function InviteOnlySignUpComponent(props : InviteOnlySignUpProps) {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const org = useOrg() as any;
const router = useRouter();
const [error, setError] = React.useState('');
const [message, setMessage] = React.useState('');
const formik = useFormik({
initialValues: {
org_slug: org?.slug,
org_id: org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async values => {
setError('')
setMessage('')
setIsSubmitting(true);
let res = await signUpWithInviteCode(values, props.inviteCode);
let message = await res.json();
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
setIsSubmitting(false);
}
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
setError(message.detail);
setIsSubmitting(false);
}
else {
setError("Something went wrong");
setIsSubmitting(false);
}
},
});
useEffect(() => {
}
, [org]);
return (
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{message && (
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
<div className='flex space-x-2'>
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<hr className='border-green-900/20 800 w-40 border' />
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage label='Username' message={formik.errors.username} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
</Form.Control>
</FormField>
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
<Form.Control asChild>
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
{isSubmitting ? "Loading..."
: "Create an account & Join"}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
)
}
export default InviteOnlySignUpComponent

View file

@ -0,0 +1,163 @@
"use client";
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import React, { useEffect } from 'react'
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
import * as Form from '@radix-ui/react-form';
import { AlertTriangle, Check, User } from 'lucide-react';
import Link from 'next/link';
import { signup } from '@services/auth/auth';
import { useOrg } from '@components/Contexts/OrgContext';
const validate = (values: any) => {
const errors: any = {};
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.username) {
errors.username = 'Required';
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters';
}
if (!values.bio) {
errors.bio = 'Required';
}
return errors;
};
function OpenSignUpComponent() {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const org = useOrg() as any;
const router = useRouter();
const [error, setError] = React.useState('');
const [message, setMessage] = React.useState('');
const formik = useFormik({
initialValues: {
org_slug: org?.slug,
org_id: org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async values => {
setError('')
setMessage('')
setIsSubmitting(true);
let res = await signup(values);
let message = await res.json();
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
setIsSubmitting(false);
}
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
setError(message.detail);
setIsSubmitting(false);
}
else {
setError("Something went wrong");
setIsSubmitting(false);
}
},
});
useEffect(() => {
}
, [org]);
return (
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{message && (
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
<div className='flex space-x-2'>
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<hr className='border-green-900/20 800 w-40 border' />
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage label='Username' message={formik.errors.username} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
</Form.Control>
</FormField>
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
<Form.Control asChild>
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
{isSubmitting ? "Loading..."
: "Create an account"}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
)
}
export default OpenSignUpComponent

View file

@ -1,8 +1,9 @@
import React from "react";
import SignUpClient from "./signup";
import { Metadata } from "next"; import { Metadata } from "next";
import { getOrganizationContextInfo } from "@services/organizations/orgs"; import { getOrganizationContextInfo } from "@services/organizations/orgs";
import SignUpClient from "./signup";
import { Suspense } from "react";
import PageLoading from "@components/Objects/Loaders/PageLoading";
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string }; params: { orgslug: string, courseid: string };
@ -14,7 +15,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const orgslug = params.orgslug; const orgslug = params.orgslug;
// Get Org context information // Get Org context information
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
return { return {
title: 'Sign up' + `${org.name}`, title: 'Sign up' + `${org.name}`,
@ -23,12 +24,14 @@ export async function generateMetadata(
const SignUp = async (params: any) => { const SignUp = async (params: any) => {
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
return ( return (
<div> <>
<SignUpClient org={org}></SignUpClient> <Suspense fallback={<PageLoading/>}>
</div> <SignUpClient org={org} />
</Suspense>
</>
); );
}; };
export default SignUp; export default SignUp;

View file

@ -1,117 +1,62 @@
"use client"; "use client";
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import learnhouseIcon from "public/learnhouse_bigicon_1.png"; import learnhouseIcon from "public/learnhouse_bigicon_1.png";
import React from 'react'
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form'
import Image from 'next/image'; import Image from 'next/image';
import * as Form from '@radix-ui/react-form';
import { getOrgLogoMediaDirectory } from '@services/media/media'; import { getOrgLogoMediaDirectory } from '@services/media/media';
import { AlertTriangle, Check, User } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { signup } from '@services/auth/auth';
import { getUriWithOrg } from '@services/config/config'; import { getUriWithOrg } from '@services/config/config';
import { useSession } from "@components/Contexts/SessionContext";
import React, { useEffect } from "react";
import { MailWarning, Shield, UserPlus } from "lucide-react";
import { useOrg } from "@components/Contexts/OrgContext";
import UserAvatar from "@components/Objects/UserAvatar";
import OpenSignUpComponent from "./OpenSignup";
import InviteOnlySignUpComponent from "./InviteOnlySignUp";
import { useRouter, useSearchParams } from "next/navigation";
import { validateInviteCode } from "@services/organizations/invites";
import PageLoading from "@components/Objects/Loaders/PageLoading";
import Toast from "@components/StyledElements/Toast/Toast";
import toast from "react-hot-toast";
interface SignUpClientProps { interface SignUpClientProps {
org: any; org: any;
} }
const validate = (values: any) => {
const errors: any = {};
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.username) {
errors.username = 'Required';
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters';
}
if (!values.bio) {
errors.bio = 'Required';
}
return errors;
};
function SignUpClient(props: SignUpClientProps) { function SignUpClient(props: SignUpClientProps) {
const [isSubmitting, setIsSubmitting] = React.useState(false); const session = useSession() as any;
const router = useRouter(); const [joinMethod, setJoinMethod] = React.useState('open');
const [error, setError] = React.useState(''); const [inviteCode, setInviteCode] = React.useState('');
const [message, setMessage] = React.useState(''); const searchParams = useSearchParams()
const formik = useFormik({ const inviteCodeParam = searchParams.get('inviteCode')
initialValues: {
org_slug: props.org?.slug,
org_id: props.org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async values => {
setError('')
setMessage('')
setIsSubmitting(true);
let res = await signup(values);
let message = await res.json();
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
setIsSubmitting(false);
}
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
setError(message.detail);
setIsSubmitting(false);
}
else {
setError("Something went wrong");
setIsSubmitting(false);
}
}, useEffect(() => {
}); if (props.org.config) {
setJoinMethod(props.org?.config?.config?.GeneralConfig.users.signup_mechanism);
console.log(props.org?.config?.config?.GeneralConfig.users.signup_mechanism)
}
if (inviteCodeParam) {
setInviteCode(inviteCodeParam);
}
}
, [props.org, inviteCodeParam]);
return ( return (
<div><div className='grid grid-flow-col justify-stretch h-screen'> <div className='grid grid-flow-col justify-stretch h-screen'>
<div className="right-login-part" style={{ background: "linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)" }} > <div className="right-login-part" style={{ background: "linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)" }} >
<div className='login-topbar m-10'> <div className='login-topbar m-10'>
<Link prefetch href={getUriWithOrg(props.org.slug, "/")}> <Link prefetch href={getUriWithOrg(props.org.slug, "/")}>
<Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" /> <Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" />
</Link> </Link>
</div> </div>
<div className="ml-10 h-4/6 flex flex-row text-white"> <div className="ml-10 h-3/4 flex flex-row text-white">
<div className="m-auto flex space-x-4 items-center flex-wrap"> <div className="m-auto flex space-x-4 items-center flex-wrap">
<div>Join </div> <div>You've been invited to join </div>
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" > <div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
{props.org?.logo ? ( {props.org?.logo_image ? (
<img <img
src={`${getOrgLogoMediaDirectory(props.org.org_id, props.org?.logo)}`} src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
alt="Learnhouse" alt="Learnhouse"
style={{ width: "auto", height: 70 }} style={{ width: "auto", height: 70 }}
className="rounded-md shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white" className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
/> />
) : ( ) : (
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" /> <Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
@ -121,70 +66,113 @@ function SignUpClient(props: SignUpClientProps) {
</div> </div>
</div> </div>
</div> </div>
<div className="left-login-part bg-white flex flex-row"> <div className="left-join-part bg-white flex flex-row">
<div className="login-form m-auto w-72"> {joinMethod == 'open' && (
{error && ( session.isAuthenticated ? <LoggedInJoinScreen inviteCode={inviteCode} /> : <OpenSignUpComponent />
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm"> )}
<AlertTriangle size={18} /> {joinMethod == 'inviteOnly' && (
<div className="font-bold text-sm">{error}</div> inviteCode ? (
</div> session.isAuthenticated ? <LoggedInJoinScreen /> : <InviteOnlySignUpComponent inviteCode={inviteCode} />
)} ) : <NoTokenScreen />
{message && ( )}
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
<div className='flex space-x-2'>
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<hr className='border-green-900/20 800 w-40 border' />
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage label='Username' message={formik.errors.username} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
</Form.Control>
</FormField>
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
<Form.Control asChild>
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
{isSubmitting ? "Loading..."
: "Create an account"}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
</div> </div>
</div></div> </div>
)
}
const LoggedInJoinScreen = (props: any) => {
const session = useSession() as any;
const org = useOrg() as any;
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
if (session && org) {
setIsLoading(false);
}
}
, [org, session]);
return (
<div className="flex flex-row items-center mx-auto">
<div className="flex space-y-7 flex-col justify-center items-center">
<p className='pt-3 text-2xl font-semibold text-black/70 flex justify-center space-x-2 items-center'>
<span className='items-center'>Hi</span>
<span className='capitalize flex space-x-2 items-center'>
<UserAvatar rounded='rounded-xl' border='border-4' width={35} />
<span>{session.user.username},</span>
</span>
<span>join {org?.name} ?</span>
</p>
<button className="flex w-fit space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md">
<UserPlus size={18} />
<p>Join </p>
</button>
</div>
</div>
)
}
const NoTokenScreen = (props: any) => {
const session = useSession() as any;
const org = useOrg() as any;
const router = useRouter();
const [isLoading, setIsLoading] = React.useState(true);
const [inviteCode, setInviteCode] = React.useState('');
const [messsage, setMessage] = React.useState('bruh');
const handleInviteCodeChange = (e: any) => {
setInviteCode(e.target.value);
}
const validateCode = async () => {
setIsLoading(true);
let res = await validateInviteCode(org?.id, inviteCode);
//wait for 1s
if (res.success) {
toast.success("Invite code is valid, you'll be redirected to the signup page in a few seconds");
setTimeout(() => {
router.push(`/signup?inviteCode=${inviteCode}`);
}, 2000);
}
else {
toast.error("Invite code is invalid");
setIsLoading(false);
}
}
useEffect(() => {
if (session && org) {
setIsLoading(false);
}
}
, [org, session]);
return (
<div className="flex flex-row items-center mx-auto">
<Toast />
{isLoading ? <div className="flex space-y-7 flex-col w-[300px] justify-center items-center"><PageLoading /></div> : <div className="flex space-y-7 flex-col justify-center items-center">
<p className="flex space-x-2 text-lg font-medium text-red-800 items-center">
<MailWarning size={18} />
<span>An invite code is required to join {org?.name}</span>
</p>
<input onChange={handleInviteCodeChange} className="bg-white outline-2 outline outline-gray-200 rounded-lg px-5 w-[300px] h-[50px]" placeholder="Please enter an invite code" type="text" />
<button onClick={validateCode} className="flex w-fit space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md">
<Shield size={18} />
<p>Submit </p>
</button>
</div>}
</div>
) )
} }

View file

@ -1,13 +1,9 @@
'use client'; 'use client';
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth'; import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import React, { useContext, createContext, useEffect } from 'react' import React, { useContext, createContext, useEffect } from 'react'
import { useOrg } from './OrgContext';
export const SessionContext = createContext({}) as any; export const SessionContext = createContext({}) as any;
const PATHS_THAT_REQUIRE_AUTH = ['/dash'];
type Session = { type Session = {
access_token: string; access_token: string;
user: any; user: any;
@ -18,10 +14,6 @@ type Session = {
function SessionProvider({ children }: { children: React.ReactNode }) { function SessionProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false }); const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false });
const org = useOrg() as any;
const pathname = usePathname()
const router = useRouter()
async function getNewAccessTokenUsingRefreshTokenUI() { async function getNewAccessTokenUsingRefreshTokenUI() {
let data = await getNewAccessTokenUsingRefreshToken(); let data = await getNewAccessTokenUsingRefreshToken();
@ -39,6 +31,10 @@ function SessionProvider({ children }: { children: React.ReactNode }) {
// Set session // Set session
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true }); setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
} }
if (!access_token) {
setSession({ access_token: "", user: {}, roles: {}, isLoading: false, isAuthenticated: false });
}
} }
@ -47,8 +43,6 @@ function SessionProvider({ children }: { children: React.ReactNode }) {
// Check session // Check session
checkSession(); checkSession();
}, []) }, [])
return ( return (

View file

@ -5,6 +5,7 @@ import * as Switch from '@radix-ui/react-switch';
import * as Form from '@radix-ui/react-form'; import * as Form from '@radix-ui/react-form';
import React from 'react' import React from 'react'
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext'; import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
import ThumbnailUpdate from './ThumbnailUpdate';
type EditCourseStructureProps = { type EditCourseStructureProps = {
@ -84,71 +85,80 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
}, [course, formik.values, formik.initialValues]); }, [course, formik.values, formik.initialValues]);
return ( return (
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'> <div> <div className="h-6"></div>
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
{course.courseStructure && ( {course.courseStructure && (
<div className="editcourse-form"> <div className="editcourse-form">
{error && ( {error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm"> <div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} /> <AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div> <div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="about">
<FormLabelAndMessage label='About' message={formik.errors.about} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
<FormField name="tags">
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
</Form.Control>
</FormField>
<FormField className="flex items-center h-10" name="public">
<div className='flex my-auto items-center'>
<label className="text-black text-[15px] leading-none pr-[15px]" htmlFor="public-course">
Public Course
</label>
<Switch.Root
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
id="public-course"
onCheckedChange={checked => formik.setFieldValue('public', checked)}
checked={formik.values.public === 'true'}
>
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div> </div>
</FormField> )}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
</FormLayout> <FormField name="description">
</div> <FormLabelAndMessage label='Description' message={formik.errors.description} />
)} <Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="about">
<FormLabelAndMessage label='About' message={formik.errors.about} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
<FormField name="tags">
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
</Form.Control>
</FormField>
<FormField name="thumbnail">
<FormLabelAndMessage label='Thumbnail' />
<Form.Control asChild>
<ThumbnailUpdate />
</Form.Control>
</FormField>
<FormField className="flex items-center h-10" name="public">
<div className='flex my-auto items-center'>
<label className="text-black text-[15px] leading-none pr-[15px]" htmlFor="public-course">
Public Course
</label>
<Switch.Root
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
id="public-course"
onCheckedChange={checked => formik.setFieldValue('public', checked)}
checked={formik.values.public === 'true'}
>
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
</FormField>
</FormLayout>
</div>
)}
</div>
</div> </div>
) )
} }

View file

@ -0,0 +1,79 @@
import { useCourse } from '@components/Contexts/CourseContext';
import { useOrg } from '@components/Contexts/OrgContext';
import { getAPIUrl } from '@services/config/config';
import { updateCourseThumbnail } from '@services/courses/courses';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { ArrowBigUpDash, UploadCloud } from 'lucide-react';
import React from 'react'
import { mutate } from 'swr';
function ThumbnailUpdate() {
const course = useCourse() as any;
const org = useOrg() as any;
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any;
const [isLoading, setIsLoading] = React.useState(false) as any;
const [error, setError] = React.useState('') as any;
const handleFileChange = async (event: any) => {
const file = event.target.files[0];
setLocalThumbnail(file);
setIsLoading(true);
const res = await updateCourseThumbnail(course.courseStructure.course_uuid, file)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
// wait for 1 second to show loading animation
await new Promise(r => setTimeout(r, 1500));
if (res.success === false) {
setError(res.HTTPmessage);
} else {
setIsLoading(false);
setError('');
}
};
return (
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow'>
<div className='flex flex-col justify-center items-center h-full'>
<div className='flex flex-col justify-center items-center'>
<div className='flex flex-col justify-center items-center'>
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
<div className="text-sm font-semibold">{error}</div>
</div>
)}
{localThumbnail ? (
<img src={URL.createObjectURL(localThumbnail)} className={`${isLoading ? 'animate-pulse' : ''} shadow w-[200px] h-[100px] rounded-md`} />
) : (
<img src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.courseStructure.course_uuid, course.courseStructure.thumbnail_image)}`} className='shadow w-[200px] h-[100px] rounded-md' />
)}
</div>
{isLoading ? (<div className='flex justify-center items-center'>
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
<div
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
>
<ArrowBigUpDash size={16} className='mr-2' />
<span>Uploading</span>
</div>
</div>) : (
<div className='flex justify-center items-center'>
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
<button
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex'
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className='mr-2' />
<span>Change Thumbnail</span>
</button>
</div>
)}
</div>
</div>
</div>
)
}
export default ThumbnailUpdate

View file

@ -92,6 +92,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<div className="h-6"></div>
{winReady ? {winReady ?
<DragDropContext onDragEnd={updateStructure}> <DragDropContext onDragEnd={updateStructure}>
<Droppable type='chapter' droppableId='chapters'> <Droppable type='chapter' droppableId='chapters'>
@ -129,7 +130,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
dialogTitle="Create chapter" dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course" dialogDescription="Add a new chapter to the course"
dialogTrigger={ dialogTrigger={
<div className="mt-4 w-44 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10"> <div className="w-44 my-16 py-5 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
<div className='mx-auto flex space-x-2 items-center hover:cursor-pointer'> <div className='mx-auto flex space-x-2 items-center hover:cursor-pointer'>
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " /> <Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
<div className='font-bold text-sm'>Add Chapter</div></div> <div className='font-bold text-sm'>Add Chapter</div></div>

View file

@ -1,10 +1,10 @@
import { useCourse } from '@components/Contexts/CourseContext' import { useCourse } from '@components/Contexts/CourseContext'
import { Book, ChevronRight, School, User } from 'lucide-react' import { Book, ChevronRight, School, User, Users } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import React, { use, useEffect } from 'react' import React, { use, useEffect } from 'react'
type BreadCrumbsProps = { type BreadCrumbsProps = {
type: 'courses' | 'user' | 'users' | 'org' type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
last_breadcrumb?: string last_breadcrumb?: string
} }
@ -17,7 +17,9 @@ function BreadCrumbs(props: BreadCrumbsProps) {
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'> <div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
<div className='flex items-center space-x-1'> <div className='flex items-center space-x-1'>
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''} {props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user/settings/general'>Account Settings</Link></div> : ''} {props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user-account/settings/general'>Account Settings</Link></div> : ''}
{props.type == 'orgusers' ? <div className='flex space-x-2 items-center'> <Users className='text-gray' size={14}></Users><Link href='/dash/users/settings/users'>Organization users</Link></div> : ''}
{props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''} {props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''}
<div className='flex items-center space-x-1 first-letter:uppercase'> <div className='flex items-center space-x-1 first-letter:uppercase'>
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''} {props.last_breadcrumb ? <ChevronRight size={17} /> : ''}

View file

@ -7,6 +7,8 @@ import { getUriWithOrg } from "@services/config/config";
import { useOrg } from "@components/Contexts/OrgContext"; import { useOrg } from "@components/Contexts/OrgContext";
import { getCourseThumbnailMediaDirectory } from "@services/media/media"; import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png';
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) { export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
const course = useCourse() as any; const course = useCourse() as any;
@ -21,7 +23,10 @@ export function CourseOverviewTop({ params }: { params: CourseOverviewParams })
<div className='flex'> <div className='flex'>
<div className='flex py-5 grow items-center'> <div className='flex py-5 grow items-center'>
<Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}> <Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}>
{course?.courseStructure?.thumbnail_image ?
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, "course_" + params.courseuuid, course.courseStructure.thumbnail_image)}`} alt="" /> <img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, "course_" + params.courseuuid, course.courseStructure.thumbnail_image)}`} alt="" />
:
<Image width={100} className="h-[57px] rounded-md drop-shadow-md" src={EmptyThumbnailImage} alt="" />}
</Link> </Link>
<div className="flex flex-col course_metadata justify-center pl-5"> <div className="flex flex-col course_metadata justify-center pl-5">
<div className='text-gray-400 font-semibold text-sm'>Course</div> <div className='text-gray-400 font-semibold text-sm'>Course</div>

View file

@ -4,12 +4,13 @@ import { useSession } from '@components/Contexts/SessionContext';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png'; import LearnHouseDashboardLogo from '@public/dashLogo.png';
import { logout } from '@services/auth/auth'; import { logout } from '@services/auth/auth';
import Avvvatars from 'avvvatars-react'; import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings } from 'lucide-react'
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React, { use, useEffect } from 'react' import React, { useEffect } from 'react'
import UserAvatar from '../../Objects/UserAvatar';
import AdminAuthorization from '@components/Security/AdminAuthorization';
function LeftMenu() { function LeftMenu() {
const org = useOrg() as any; const org = useOrg() as any;
@ -42,8 +43,8 @@ function LeftMenu() {
return ( return (
<div <div
style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.20) 0%, rgba(0, 0, 0, 0.20) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0.00) 100%), #2E2D2D" }} style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)" }}
className='flex flex-col w-28 bg-black h-screen text-white shadow-xl'> className='flex flex-col w-[90px] bg-black h-screen text-white shadow-xl'>
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
<div className='flex h-20 mt-6'> <div className='flex h-20 mt-6'>
<Link className='flex flex-col items-center mx-auto space-y-3' href={"/"}> <Link className='flex flex-col items-center mx-auto space-y-3' href={"/"}>
@ -59,27 +60,32 @@ function LeftMenu() {
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' > {/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link> <Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
</ToolTip> */} </ToolTip> */}
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' > <AdminAuthorization authorizationMode="component">
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link> <ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
</ToolTip> <Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' > </ToolTip>
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link> <ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
</ToolTip> <Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' > </ToolTip>
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link> <ToolTip content={"Users"} slateBlack sideOffset={8} side='right' >
</ToolTip> <Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/users/settings/users`} ><Users size={18} /></Link>
</ToolTip>
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
</ToolTip>
</AdminAuthorization>
</div> </div>
<div className='flex flex-col mx-auto pb-7 space-y-2'> <div className='flex flex-col mx-auto pb-7 space-y-2'>
<div className="flex items-center flex-col space-y-2"> <div className="flex items-center flex-col space-y-2">
<ToolTip content={session.user.username} slateBlack sideOffset={8} side='right' > <ToolTip content={'@' + session.user.username} slateBlack sideOffset={8} side='right' >
<div className="mx-auto shadow-lg"> <div className='mx-auto'>
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" /> <UserAvatar border='border-4' width={35} />
</div> </div>
</ToolTip> </ToolTip>
<div className='flex items-center flex-col space-y-1'> <div className='flex items-center flex-col space-y-1'>
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' > <ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
<Link href={'/dash/user/settings/general'} className='py-3'> <Link href={'/dash/user-account/settings/general'} className='py-3'>
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} /> <Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
</Link> </Link>
</ToolTip> </ToolTip>

View file

@ -1,100 +0,0 @@
import { updateProfile } from '@services/settings/profile';
import React, { useEffect } from 'react'
import { Formik, Form, Field, ErrorMessage } from 'formik';
import { useSession } from '@components/Contexts/SessionContext';
function UserEditGeneral() {
const session = useSession() as any;
useEffect(() => {
}
, [session, session.user])
return (
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
{session.user && (
<Formik
enableReinitialize
initialValues={{
username: session.user.username,
first_name: session.user.first_name,
last_name: session.user.last_name,
email: session.user.email,
bio: session.user.bio,
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
updateProfile(values,session.user.id)
}, 400);
}}
>
{({ isSubmitting }) => (
<Form className="max-w-md">
<label className="block mb-2 font-bold" htmlFor="email">
Email
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="email"
name="email"
/>
<label className="block mb-2 font-bold" htmlFor="username">
Username
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="username"
name="username"
/>
<label className="block mb-2 font-bold" htmlFor="first_name">
First Name
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="first_name"
name="first_name"
/>
<label className="block mb-2 font-bold" htmlFor="last_name">
Last Name
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="last_name"
name="last_name"
/>
<label className="block mb-2 font-bold" htmlFor="bio">
Bio
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="bio"
name="bio"
/>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Submit
</button>
</Form>
)}
</Formik>
)}
</div>
)
}
export default UserEditGeneral

View file

@ -0,0 +1,181 @@
import { updateProfile } from '@services/settings/profile';
import React, { useEffect } from 'react'
import { Formik, Form, Field, ErrorMessage } from 'formik';
import { useSession } from '@components/Contexts/SessionContext';
import { ArrowBigUpDash, Check, FileWarning, Info, UploadCloud } from 'lucide-react';
import UserAvatar from '@components/Objects/UserAvatar';
import { updateUserAvatar } from '@services/users/users';
function UserEditGeneral() {
const session = useSession() as any;
const [localAvatar, setLocalAvatar] = React.useState(null) as any;
const [isLoading, setIsLoading] = React.useState(false) as any;
const [error, setError] = React.useState() as any;
const [success, setSuccess] = React.useState('') as any;
const handleFileChange = async (event: any) => {
const file = event.target.files[0];
setLocalAvatar(file);
setIsLoading(true);
const res = await updateUserAvatar(session.user.user_uuid, file)
// wait for 1 second to show loading animation
await new Promise(r => setTimeout(r, 1500));
if (res.success === false) {
setError(res.HTTPmessage);
} else {
setIsLoading(false);
setError('');
setSuccess('Avatar Updated');
}
};
useEffect(() => {
}
, [session, session.user])
return (
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
{session.user && (
<Formik
enableReinitialize
initialValues={{
username: session.user.username,
first_name: session.user.first_name,
last_name: session.user.last_name,
email: session.user.email,
bio: session.user.bio,
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
updateProfile(values, session.user.id)
}, 400);
}}
>
{({ isSubmitting }) => (
<div className='flex space-x-8'>
<Form className="max-w-md">
<label className="block mb-2 font-bold" htmlFor="email">
Email
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="email"
name="email"
/>
<label className="block mb-2 font-bold" htmlFor="username">
Username
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="username"
name="username"
/>
<label className="block mb-2 font-bold" htmlFor="first_name">
First Name
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="first_name"
name="first_name"
/>
<label className="block mb-2 font-bold" htmlFor="last_name">
Last Name
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="last_name"
name="last_name"
/>
<label className="block mb-2 font-bold" htmlFor="bio">
Bio
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="bio"
name="bio"
/>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Submit
</button>
</Form>
<div className='flex flex-col grow justify-center align-middle space-y-3'>
<label className="flex mx-auto mb-2 font-bold " >
Avatar
</label>
{error && (
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
<FileWarning size={16} className='mr-2' />
<div className="text-sm font-semibold first-letter:uppercase">{error}</div>
</div>
)}
{success && (
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
<Check size={16} className='mr-2' />
<div className="text-sm font-semibold first-letter:uppercase">{success}</div>
</div>
)}
<div className="flex flex-col space-y-3">
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20'>
<div className='flex flex-col justify-center items-center mt-10'>
{localAvatar ? (
<UserAvatar border='border-8' width={100} avatar_url={URL.createObjectURL(localAvatar)} />
) : (
<UserAvatar border='border-8' width={100} />
)}
</div>
{isLoading ? (<div className='flex justify-center items-center'>
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
<div
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
>
<ArrowBigUpDash size={16} className='mr-2' />
<span>Uploading</span>
</div>
</div>) : (
<div className='flex justify-center items-center'>
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
<button
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className='mr-2' />
<span>Change Thumbnail</span>
</button>
</div> )}
</div>
<div className='flex text-xs space-x-2 items-center text-gray-500 justify-center'>
<Info size={13} /><p>Recommended size 100x100</p>
</div>
</div>
</div>
</div>
)}
</Formik>
)}
</div>
)
}
export default UserEditGeneral

View file

@ -0,0 +1,175 @@
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import { Globe, Shield, X } from 'lucide-react'
import Link from 'next/link';
import React, { use, useEffect } from 'react'
import useSWR, { mutate } from 'swr';
import dayjs from 'dayjs';
import { changeSignupMechanism, createInviteCode, deleteInviteCode } from '@services/organizations/invites';
import Toast from '@components/StyledElements/Toast/Toast';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
function OrgAccess() {
const org = useOrg() as any;
const { data: invites } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null, swrFetcher);
const [isLoading, setIsLoading] = React.useState(false)
const [joinMethod, setJoinMethod] = React.useState('closed')
const router = useRouter()
async function getOrgJoinMethod() {
if (org) {
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
setJoinMethod('open')
}
else {
setJoinMethod('inviteOnly')
}
}
}
async function createInvite() {
let res = await createInviteCode(org.id)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
async function deleteInvite(invite: any) {
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
let res = await changeSignupMechanism(org.id, method)
if (res.status == 200) {
router.refresh()
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
useEffect(() => {
if (invites && org) {
getOrgJoinMethod()
setIsLoading(false)
}
}
, [org, invites])
return (
<>
<Toast></Toast>
{!isLoading ? (<>
<div className="h-6"></div>
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit '>
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 '>
<h1 className='font-bold text-xl text-gray-800'>Join method</h1>
<h2 className='text-gray-500 text-md'> Choose how users can join your organization </h2>
</div>
<div className='flex space-x-2 mx-auto'>
<ConfirmationModal
confirmationButtonText='Change to open '
confirmationMessage='Are you sure you want to change the signup mechanism to open ? This will allow users to join your organization freely.'
dialogTitle={'Change to open ?'}
dialogTrigger={
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
{joinMethod == 'open' ? <div className='bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg'>Active</div> : null}
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
<Globe className='text-slate-400' size={40}></Globe>
<div className='text-2xl text-slate-700 font-bold'>Open</div>
<div className='text-gray-400 text-center'>Users can join freely from the signup page</div>
</div>
</div>}
functionToExecute={() => { changeJoinMethod('open') }}
status='info'
></ConfirmationModal>
<ConfirmationModal
confirmationButtonText='Change to closed '
confirmationMessage='Are you sure you want to change the signup mechanism to closed ? This will allow users to join your organization only by invitation.'
dialogTitle={'Change to closed ?'}
dialogTrigger={
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
{joinMethod == 'inviteOnly' ? <div className='bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg'>Active</div> : null}
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
<Shield className='text-slate-400' size={40}></Shield>
<div className='text-2xl text-slate-700 font-bold'>Closed</div>
<div className='text-gray-400 text-center'>Users can join only by invitation</div>
</div>
</div>}
functionToExecute={() => { changeJoinMethod('inviteOnly') }}
status='info'
></ConfirmationModal>
</div>
<div className={joinMethod == 'open' ? 'opacity-20 pointer-events-none' : 'pointer-events-auto'}>
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 '>
<h1 className='font-bold text-xl text-gray-800'>Invite codes</h1>
<h2 className='text-gray-500 text-md'>Invite codes can be copied and used to join your organization </h2>
</div>
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
<thead className='bg-gray-100 text-gray-500 rounded-xl uppercase'>
<tr className='font-bolder text-sm'>
<th className='py-3 px-4'>Code</th>
<th className='py-3 px-4'>Signup link</th>
<th className='py-3 px-4'>Expiration date</th>
<th className='py-3 px-4'>Actions</th>
</tr>
</thead>
<>
<tbody className='mt-5 bg-white rounded-md' >
{invites?.map((invite: any) => (
<tr key={invite.invite_code_uuid} className='border-b border-gray-100 text-sm'>
<td className='py-3 px-4'>{invite.invite_code}</td>
<td className='py-3 px-4 '>
<Link className='outline bg-gray-50 text-gray-600 px-2 py-1 rounded-md outline-gray-300 outline-dashed outline-1' target='_blank' href={getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}>
{getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}
</Link>
</td>
<td className='py-3 px-4'>{dayjs(invite.expiration_date).add(1, 'year').format('DD/MM/YYYY')} </td>
<td className='py-3 px-4'>
<ConfirmationModal
confirmationButtonText='Delete Code'
confirmationMessage='Are you sure you want remove this invite code ?'
dialogTitle={'Delete code ?'}
dialogTrigger={
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
<X className='w-4 h-4' />
<span> Delete code</span>
</button>}
functionToExecute={() => { deleteInvite(invite) }}
status='warning'
></ConfirmationModal>
</td>
</tr>
))}
</tbody>
</>
</table>
<button onClick={() => createInvite()} className='mt-3 mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100'>
<Shield className='w-4 h-4' />
<span> Create invite code</span>
</button>
</div>
</div></>) : <PageLoading />}
</>
)
}
export default OrgAccess

View file

@ -0,0 +1,120 @@
import { useOrg } from '@components/Contexts/OrgContext';
import PageLoading from '@components/Objects/Loaders/PageLoading';
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate';
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import Modal from '@components/StyledElements/Modal/Modal';
import Toast from '@components/StyledElements/Toast/Toast';
import { getAPIUrl } from '@services/config/config';
import { removeUserFromOrg } from '@services/organizations/orgs';
import { swrFetcher } from '@services/utils/ts/requests';
import { KeyRound, LogOut, X } from 'lucide-react';
import React, { use, useEffect } from 'react'
import toast from 'react-hot-toast';
import useSWR, { mutate } from 'swr';
function OrgUsers() {
const org = useOrg() as any;
const { data: orgUsers } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/users` : null, swrFetcher);
const [rolesModal, setRolesModal] = React.useState(false);
const [selectedUser, setSelectedUser] = React.useState(null) as any;
const [isLoading, setIsLoading] = React.useState(true);
const handleRolesModal = (user_uuid: any) => {
setSelectedUser(user_uuid);
setRolesModal(!rolesModal);
}
const handleRemoveUser = async (user_id: any) => {
const res = await removeUserFromOrg(org.id, user_id);
if (res.status === 200) {
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
useEffect(() => {
if (orgUsers) {
setIsLoading(false)
console.log(orgUsers)
}
}, [org, orgUsers])
return (
<div>
{isLoading ? <div><PageLoading /></div> :
<>
<Toast></Toast>
<div className="h-6"></div>
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 '>
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 '>
<h1 className='font-bold text-xl text-gray-800'>Active users</h1>
<h2 className='text-gray-500 text-md'> Manage your organization users, assign roles and permissions </h2>
</div>
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
<thead className='bg-gray-100 text-gray-500 rounded-xl uppercase'>
<tr className='font-bolder text-sm'>
<th className='py-3 px-4'>User</th>
<th className='py-3 px-4'>Role</th>
<th className='py-3 px-4'>Actions</th>
</tr>
</thead>
<>
<tbody className='mt-5 bg-white rounded-md' >
{orgUsers?.map((user: any) => (
<tr key={user.user.id} className='border-b border-gray-200 border-dashed'>
<td className='py-3 px-4 flex space-x-2 items-center'>
<span>{user.user.first_name + ' ' + user.user.last_name}</span>
<span className='text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold'>@{user.user.username}</span>
</td>
<td className='py-3 px-4'>{user.role.name}</td>
<td className='py-3 px-4 flex space-x-2 items-end'>
<Modal
isDialogOpen={rolesModal && selectedUser === user.user.user_uuid}
onOpenChange={() => handleRolesModal(user.user.user_uuid)}
minHeight="no-min"
dialogContent={
<RolesUpdate
alreadyAssignedRole={user.role.role_uuid}
setRolesModal={setRolesModal}
user={user} />
}
dialogTitle="Update Role"
dialogDescription={"Update @" + user.user.username + "'s role"}
dialogTrigger={
<button className='flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100'>
<KeyRound className='w-4 h-4' />
<span> Edit Role</span>
</button>}
/>
<ConfirmationModal
confirmationButtonText='Remove User'
confirmationMessage='Are you sure you want remove this user from the organization?'
dialogTitle={'Delete ' + user.user.username + ' ?'}
dialogTrigger={
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
<LogOut className='w-4 h-4' />
<span> Remove from organization</span>
</button>}
functionToExecute={() => { handleRemoveUser(user.user.id) }}
status='warning'
></ConfirmationModal>
</td>
</tr>
))}
</tbody>
</>
</table>
</div>
</>
}
</div>
)
}
export default OrgUsers

View file

@ -1,6 +1,6 @@
import { useSession } from '@components/Contexts/SessionContext' import { useSession } from '@components/Contexts/SessionContext'
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai'; import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react'; import { AlertTriangle, BadgeInfo, NotebookTabs, User } from 'lucide-react';
import Avvvatars from 'avvvatars-react'; import Avvvatars from 'avvvatars-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FlaskConical, Keyboard, MessageCircle, MessageSquareIcon, Sparkle, Sparkles, X } from 'lucide-react' import { FlaskConical, Keyboard, MessageCircle, MessageSquareIcon, Sparkle, Sparkles, X } from 'lucide-react'
@ -13,6 +13,7 @@ import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@compon
import FeedbackModal from '@components/Objects/Modals/Feedback/Feedback'; import FeedbackModal from '@components/Objects/Modals/Feedback/Feedback';
import Modal from '@components/StyledElements/Modal/Modal'; import Modal from '@components/StyledElements/Modal/Modal';
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures'; import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
import UserAvatar from '@components/Objects/UserAvatar';
type AIActivityAskProps = { type AIActivityAskProps = {
@ -172,7 +173,7 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
</div> </div>
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'> <div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
<FlaskConical size={14} /> <FlaskConical size={14} />
<span className='text-xs font-semibold '>Experimental</span> <span className='text-xs font-semibold antialiased '>Experimental</span>
</div> </div>
</div> </div>
@ -204,7 +205,7 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
} }
<div className='flex space-x-2 items-center'> <div className='flex space-x-2 items-center'>
<div className=''> <div className=''>
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" /> <UserAvatar rounded='rounded-lg' border='border-2' width={35} />
</div> </div>
<div className='w-full'> <div className='w-full'>
<input onKeyDown={handleKeyDown} onChange={handleChange} disabled={aiChatBotState.isWaitingForResponse} value={aiChatBotState.chatInputValue} placeholder='Ask AI About this Lecture' type="text" className={inputClass} name="" id="" /> <input onKeyDown={handleKeyDown} onChange={handleChange} disabled={aiChatBotState.isWaitingForResponse} value={aiChatBotState.chatInputValue} placeholder='Ask AI About this Lecture' type="text" className={inputClass} name="" id="" />
@ -235,7 +236,11 @@ function AIMessage(props: AIMessageProps) {
return ( return (
<div className='flex space-x-2 w-full antialiased font-medium'> <div className='flex space-x-2 w-full antialiased font-medium'>
<div className=''> <div className=''>
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={props.message.type == 'ai' ? 'ai' : session.user.user_uuid} style="shape" /> {props.message.sender == 'ai' ? (
<UserAvatar rounded='rounded-lg' border='border-2' predefined_avatar='ai' width={35} />
) : (
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
)}
</div> </div>
<div className='w-full'> <div className='w-full'>
<p className='w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30' id=""> <p className='w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30' id="">
@ -277,7 +282,8 @@ const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }
<Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" /> <Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" />
<p className='pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center'> <p className='pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center'>
<span className='items-center'>Hello</span> <span className='items-center'>Hello</span>
<span className='capitalize flex space-x-2 items-center'> <Avvvatars radius={3} border borderColor='white' borderSize={3} size={25} value={session.user.user_uuid} style="shape" /> <span className='capitalize flex space-x-2 items-center'>
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
<span>{session.user.username},</span> <span>{session.user.username},</span>
</span> </span>
<span>how can we help today ?</span> <span>how can we help today ?</span>

View file

@ -57,7 +57,7 @@ function AIEditorToolkit(props: AIEditorToolkitProps) {
<div className='pr-1'> <div className='pr-1'>
<div className='flex w-full space-x-2 font-bold text-white/80 items-center'> <div className='flex w-full space-x-2 font-bold text-white/80 items-center'>
<Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" /> <Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" />
<div >AI Editor</div> <div className='flex items-center'>AI Editor <span className='text-[10px] px-2 py-1 rounded-3xl ml-3 bg-white/10 uppercase'>PRE-ALPHA</span></div>
<MoreVertical className='text-white/50' size={12} /> <MoreVertical className='text-white/50' size={12} />
</div> </div>
</div> </div>

View file

@ -42,6 +42,7 @@ import { CourseProvider } from "@components/Contexts/CourseContext";
import { useSession } from "@components/Contexts/SessionContext"; import { useSession } from "@components/Contexts/SessionContext";
import AIEditorToolkit from "./AI/AIEditorToolkit"; import AIEditorToolkit from "./AI/AIEditorToolkit";
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures"; import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
import UserAvatar from "../UserAvatar";
interface Editor { interface Editor {
@ -163,7 +164,7 @@ function Editor(props: Editor) {
<Link href="/"> <Link href="/">
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" /> <EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
</Link> </Link>
<Link target="_blank" href={`/course/${course_uuid}/edit`}> <Link target="_blank" href={`/course/${course_uuid}`}>
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail> <EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
</Link> </Link>
<EditorInfoDocName> <EditorInfoDocName>
@ -207,7 +208,7 @@ function Editor(props: Editor) {
<EditorUserProfileWrapper> <EditorUserProfileWrapper>
{!session.isAuthenticated && <span>Loading</span>} {!session.isAuthenticated && <span>Loading</span>}
{session.isAuthenticated && <Avvvatars value={session.user.user_uuid} style="shape" />} {session.isAuthenticated && <UserAvatar width={40} border="border-4" rounded="rounded-full"/>}
</EditorUserProfileWrapper> </EditorUserProfileWrapper>
</EditorUsersSection> </EditorUsersSection>

View file

@ -9,6 +9,7 @@ import { usePathname } from "next/navigation";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import path from "path"; import path from "path";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import UserAvatar from "@components/Objects/UserAvatar";
export interface Auth { export interface Auth {
access_token: string; access_token: string;
@ -91,7 +92,7 @@ function ProfileArea() {
<AccountArea> <AccountArea>
<div>{auth.userInfo.user_object.username}</div> <div>{auth.userInfo.user_object.username}</div>
<div> <div>
<Avvvatars value={auth.userInfo.user_object.user_id} style="shape" /> <UserAvatar width={40} />
</div> </div>
<Link href={"/dash"}><Settings /></Link> <Link href={"/dash"}><Settings /></Link>
</AccountArea> </AccountArea>

View file

@ -111,22 +111,22 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
<FormMessage match="valueMissing">Please provide a thumbnail for your course</FormMessage> <FormMessage match="valueMissing">Please provide a thumbnail for your course</FormMessage>
</Flex> </Flex>
<Form.Control asChild> <Form.Control asChild>
<Input onChange={handleThumbnailChange} type="file" required /> <Input onChange={handleThumbnailChange} type="file" />
</Form.Control> </Form.Control>
</FormField> </FormField>
<FormField name="course-tags"> <FormField name="course-tags">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}> <Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Course tags (separated by comma)</FormLabel> <FormLabel>Course Learnings (separated by comma)</FormLabel>
<FormMessage match="valueMissing">Please provide learning elements, separated by comma (,)</FormMessage> <FormMessage match="valueMissing">Please provide learning elements, separated by comma (,)</FormMessage>
</Flex> </Flex>
<Form.Control asChild> <Form.Control asChild>
<Textarea onChange={handleTagsChange} required /> <Textarea onChange={handleTagsChange} />
</Form.Control> </Form.Control>
</FormField> </FormField>
<FormField name="course-visibility"> <FormField name="course-visibility">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}> <Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Course Visibility</FormLabel> <FormLabel>Course Visibility</FormLabel>
<FormMessage match="valueMissing">Please choose cours visibility</FormMessage> <FormMessage match="valueMissing">Please choose course visibility</FormMessage>
</Flex> </Flex>
<Form.Control asChild> <Form.Control asChild>
<select onChange={handleVisibilityChange} className='border border-gray-300 rounded-md p-2' required> <select onChange={handleVisibilityChange} className='border border-gray-300 rounded-md p-2' required>

View file

@ -0,0 +1,82 @@
'use client';
import { useOrg } from '@components/Contexts/OrgContext';
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, Input, Textarea } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form'
import { FormMessage } from "@radix-ui/react-form";
import { getAPIUrl } from '@services/config/config';
import { updateUserRole } from '@services/organizations/orgs';
import { swrFetcher } from '@services/utils/ts/requests';
import React, { useEffect } from 'react'
import { BarLoader } from 'react-spinners';
import { mutate } from 'swr';
interface Props {
user: any
setRolesModal: any
alreadyAssignedRole: any
}
function RolesUpdate(props: Props) {
const org = useOrg() as any;
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [assignedRole, setAssignedRole] = React.useState(props.alreadyAssignedRole);
const [error, setError] = React.useState(null) as any;
const handleAssignedRole = (event: React.ChangeEvent<any>) => {
setError(null);
setAssignedRole(event.target.value);
}
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsSubmitting(true);
const res = await updateUserRole(org.id, props.user.user.id, assignedRole);
if (res.status === 200) {
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
props.setRolesModal(false);
}
else {
setIsSubmitting(false);
setError('Error ' + res.status + ': ' + res.data.detail);
}
};
useEffect(() => {
}
, [assignedRole])
return (
<div>
<FormLayout onSubmit={handleSubmit}>
<FormField name="course-visibility">
{error ? <div className='text-red-500 font-bold text-xs px-3 py-2 bg-red-100 rounded-md'>{error}</div> : ''}
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Roles</FormLabel>
<FormMessage match="valueMissing">Please choose a role for the user</FormMessage>
</Flex>
<Form.Control asChild>
<select onChange={handleAssignedRole} defaultValue={assignedRole} className='border border-gray-300 rounded-md p-2' required>
<option value="role_global_admin">Admin </option>
<option value="role_global_maintainer">Maintainer</option>
<option value="role_global_user">User</option>
</select>
</Form.Control>
</FormField>
<div className='h-full'></div>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
: "Update user role"}
</ButtonBlack>
</Form.Submit>
</Flex>
</FormLayout>
</div>
)
}
export default RolesUpdate

View file

@ -41,9 +41,10 @@ function CourseThumbnail(props: PropsType) {
<div className='relative'> <div className='relative'>
<AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_uuid} deleteCourses={deleteCourses} /> <AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_uuid} deleteCourses={deleteCourses} />
<Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_uuid))}> <Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_uuid))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)})` }}>
</div> {props.course.thumbnail_image ? <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)})` }} />
: <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url('../empty_thumbnail.png')` , backgroundSize:'contain' }} />}
</Link> </Link>
<h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2> <h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2>
</div> </div>

View file

@ -0,0 +1,65 @@
import { useSession } from '@components/Contexts/SessionContext';
import emptyAvatar from '@public/empty_avatar.png';
import aiAvatar from '@public/ai_avatar.png';
import Image from 'next/image';
import React, { use, useEffect } from 'react'
import { getUriWithOrg } from '@services/config/config';
import { useOrg } from '@components/Contexts/OrgContext';
import { useParams } from 'next/navigation';
import { getUserAvatarMediaDirectory } from '@services/media/media';
type UserAvatarProps = {
width?: number
avatar_url?: string
use_with_session?: boolean
rounded?: 'rounded-md' | 'rounded-xl' | 'rounded-lg' | 'rounded-full' | 'rounded'
border?: 'border-2' | 'border-4' | 'border-8'
borderColor? : string
predefined_avatar?: 'ai'
}
function UserAvatar(props: UserAvatarProps) {
const session = useSession() as any;
const params = useParams() as any;
const predefinedAvatar = props.predefined_avatar === 'ai' ? getUriWithOrg(params.orgslug,'/ai_avatar.png') : null;
const emptyAvatar = getUriWithOrg(params.orgslug,'/empty_avatar.png') as any;
const uploadedAvatar = getUserAvatarMediaDirectory(session.user.user_uuid,session.user.avatar_image) as any;
const useAvatar = () => {
if (props.predefined_avatar) {
return predefinedAvatar
} else {
if (props.avatar_url) {
console.log('avatar_url',props.avatar_url)
return props.avatar_url
}
else {
if (session.user.avatar_image) {
return uploadedAvatar
}
else {
return emptyAvatar
}
}
}
}
useEffect(() => {
console.log('params', params)
}
, [session])
return (
<img
alt='User Avatar'
width={props.width ? props.width : 50}
height={props.width ? props.width : 50}
src={useAvatar()}
className={`${props.avatar_url && session.user.avatar_image ? '' : 'bg-gray-700'} ${props.border ? 'border ' + props.border : ''} ${props.borderColor ? props.borderColor : 'border-white'} shadow-xl aspect-square w-[${props.width ? props.width : 50}px] h-[${props.width ? props.width : 50}px] ${props.rounded ? props.rounded : 'rounded-xl'}`}
/>
)
}
export default UserAvatar

View file

@ -0,0 +1,139 @@
'use client';
import { useOrg } from '@components/Contexts/OrgContext';
import { useSession } from '@components/Contexts/SessionContext';
import { usePathname, useRouter } from 'next/navigation';
import React from 'react'
type AuthorizationProps = {
children: React.ReactNode;
// Authorize components rendering or page rendering
authorizationMode: 'component' | 'page';
}
const ADMIN_PATHS = [
'/dash/org/*',
'/dash/org',
'/dash/users/*',
'/dash/users',
'/dash/courses/*',
'/dash/courses',
'/dash/org/settings/general',
]
function AdminAuthorization(props: AuthorizationProps) {
const session = useSession() as any;
const org = useOrg() as any;
const pathname = usePathname();
const router = useRouter();
// States
const [isLoading, setIsLoading] = React.useState(true);
const [isAuthorized, setIsAuthorized] = React.useState(false);
// Verify if the user is authenticated
const isUserAuthenticated = () => {
if (session.isAuthenticated === true) {
return true;
}
else {
return false;
}
}
// Verify if the user is an Admin (1), Maintainer (2) or Member (3) of the organization
const isUserAdmin = () => {
const isAdmin = session.roles.some((role: any) => {
return (
role.org.id === org.id &&
(role.role.id === 1 ||
role.role.id === 2 ||
role.role.role_uuid === 'role_global_admin' ||
role.role.role_uuid === 'role_global_maintainer'
)
);
});
return isAdmin;
};
function checkPathname(pattern: string, pathname: string) {
// Escape special characters in the pattern and replace '*' with a regex pattern
const regexPattern = new RegExp(`^${pattern.replace(/\//g, '\\/').replace(/\*/g, '.*')}$`);
// Test if the pathname matches the regex pattern
const isMatch = regexPattern.test(pathname);
return isMatch;
}
const Authorize = () => {
if (props.authorizationMode === 'page') {
// Check if user is in an admin path
if (ADMIN_PATHS.some((path) => checkPathname(path, pathname))) {
console.log('Admin path')
if (isUserAuthenticated()) {
// Check if the user is an Admin
if (isUserAdmin()) {
setIsAuthorized(true);
}
else {
setIsAuthorized(false);
router.push('/dash');
}
}
else {
router.push('/login');
}
}
else {
if (isUserAuthenticated()) {
setIsAuthorized(true);
}
else {
setIsAuthorized(false);
router.push('/login');
}
}
}
if (props.authorizationMode === 'component') {
// Component mode
if (isUserAuthenticated() && isUserAdmin()) {
setIsAuthorized(true);
}
else {
setIsAuthorized(false);
}
}
}
React.useEffect(() => {
if (session.isLoading) {
return;
}
Authorize();
setIsLoading(false);
}, [session, org, pathname])
return (
<>
{props.authorizationMode === 'component' && isAuthorized === true && props.children}
{props.authorizationMode === 'page' && isAuthorized === true && !isLoading && props.children}
{props.authorizationMode === 'page' && isAuthorized === false && !isLoading &&
<div className='flex justify-center items-center h-screen'>
<h1 className='text-2xl'>You are not authorized to access this page</h1>
</div>
}
</>
)
}
export default AdminAuthorization

View file

@ -1,8 +1,5 @@
'use client'; 'use client';
import React from "react"; import React from "react";
import useSWR, { mutate } from "swr";
import { getAPIUrl } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests";
import { useSession } from "@components/Contexts/SessionContext"; import { useSession } from "@components/Contexts/SessionContext";
import { useOrg } from "@components/Contexts/OrgContext"; import { useOrg } from "@components/Contexts/OrgContext";
@ -20,18 +17,18 @@ export const AuthenticatedClientElement = (props: AuthenticatedClientElementProp
const [isAllowed, setIsAllowed] = React.useState(false); const [isAllowed, setIsAllowed] = React.useState(false);
const session = useSession() as any; const session = useSession() as any;
const org = useOrg() as any; const org = useOrg() as any;
function isUserAllowed(roles: any[], action: string, resourceType: string, org_uuid: string): boolean { function isUserAllowed(roles: any[], action: string, resourceType: string, org_uuid: string): boolean {
// Iterate over the user's roles // Iterate over the user's roles
for (const role of roles) { for (const role of roles) {
// Check if the role is for the right organization // Check if the role is for the right organization
if (role.org.org_uuid === org_uuid) { if (role.org.org_uuid === org_uuid) {
// Check if the user has the role for the resource type // Check if the user has the role for the resource type
if (role.role.rights && role.role.rights[resourceType]) { if (role.role.rights && role.role.rights[resourceType]) {
// Check if the user is allowed to execute the action // Check if the user is allowed to execute the action
const actionKey = `action_${action}`; const actionKey = `action_${action}`;
if (role.role.rights[resourceType][actionKey] === true) { if (role.role.rights[resourceType][actionKey] === true) {
@ -46,11 +43,16 @@ export const AuthenticatedClientElement = (props: AuthenticatedClientElementProp
} }
function check() { function check() {
if (session.isAuthenticated === false) {
if (props.checkMethod === 'authentication') { setIsAllowed(false);
setIsAllowed(session.isAuthenticated); return;
} else if (props.checkMethod === 'roles') { }
return setIsAllowed(isUserAllowed(session.roles, props.action!, props.ressourceType!, org.org_uuid)); else {
if (props.checkMethod === 'authentication') {
setIsAllowed(session.isAuthenticated);
} else if (props.checkMethod === 'roles') {
return setIsAllowed(isUserAllowed(session.roles, props.action!, props.ressourceType!, org.org_uuid));
}
} }
} }

View file

@ -1,11 +1,12 @@
'use client'; 'use client';
import React, { use, useEffect } from "react"; import React, { useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import Link from "next/link"; import Link from "next/link";
import Avvvatars from "avvvatars-react"; import Avvvatars from "avvvatars-react";
import { GearIcon } from "@radix-ui/react-icons"; import { GearIcon } from "@radix-ui/react-icons";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { useSession } from "@components/Contexts/SessionContext"; import { useSession } from "@components/Contexts/SessionContext";
import UserAvatar from "@components/Objects/UserAvatar";
export const HeaderProfileBox = () => { export const HeaderProfileBox = () => {
const session = useSession() as any; const session = useSession() as any;
@ -33,9 +34,7 @@ export const HeaderProfileBox = () => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="text-xs">{session.user.username} </div> <div className="text-xs">{session.user.username} </div>
<div className="py-4"> <div className="py-4">
<div className="shadow-sm rounded-xl"> <UserAvatar border="border-4" rounded='rounded-lg' width={30} />
<Avvvatars radius={3} size={30} value={session.user.user_uuid} style="shape" />
</div>
</div> </div>
<Link className="text-gray-600" href={"/dash"}><Settings size={14} /></Link> <Link className="text-gray-600" href={"/dash"}><Settings size={14} /></Link>
</div> </div>
@ -51,7 +50,6 @@ const AccountArea = styled.div`
img { img {
width: 29px; width: 29px;
border-radius: 19px;
} }
`; `;

View file

@ -1,22 +1,20 @@
import { XCircle } from 'lucide-react'
import React from 'react' import React from 'react'
function ErrorUI() { function ErrorUI() {
return ( return (
<div className='flex items-center justify-center h-screen'> <div className='flex items-center justify-center h-screen'>
<div className='mx-auto bg-red-100 w-[800px] p-3 rounded-xl m-5 '> <div className='mx-auto bg-red-100 w-[800px] p-3 rounded-xl m-5 '>
<div className='flex flex-row'> <div className='flex flex-row'>
<div className='p-3 pr-4' > <div className='p-3 pr-4 items-center' >
<svg width="35" height="35" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <XCircle size={40} className='text-red-600' />
<path d="M19 2H5C4.20435 2 3.44129 2.31607 2.87868 2.87868C2.31607 3.44129 2 4.20435 2 5V15C2 15.7956 2.31607 16.5587 2.87868 17.1213C3.44129 17.6839 4.20435 18 5 18H16.59L20.29 21.71C20.3834 21.8027 20.4943 21.876 20.6161 21.9258C20.7379 21.9755 20.8684 22.0008 21 22C21.1312 22.0034 21.2613 21.976 21.38 21.92C21.5626 21.845 21.7189 21.7176 21.8293 21.5539C21.9396 21.3901 21.999 21.1974 22 21V5C22 4.20435 21.6839 3.44129 21.1213 2.87868C20.5587 2.31607 19.7956 2 19 2ZM20 18.59L17.71 16.29C17.6166 16.1973 17.5057 16.124 17.3839 16.0742C17.2621 16.0245 17.1316 15.9992 17 16H5C4.73478 16 4.48043 15.8946 4.29289 15.7071C4.10536 15.5196 4 15.2652 4 15V5C4 4.73478 4.10536 4.48043 4.29289 4.29289C4.48043 4.10536 4.73478 4 5 4H19C19.2652 4 19.5196 4.10536 19.7071 4.29289C19.8946 4.48043 20 4.73478 20 5V18.59ZM12 12C11.8022 12 11.6089 12.0586 11.4444 12.1685C11.28 12.2784 11.1518 12.4346 11.0761 12.6173C11.0004 12.8 10.9806 13.0011 11.0192 13.1951C11.0578 13.3891 11.153 13.5673 11.2929 13.7071C11.4327 13.847 11.6109 13.9422 11.8049 13.9808C11.9989 14.0194 12.2 13.9996 12.3827 13.9239C12.5654 13.8482 12.7216 13.72 12.8315 13.5556C12.9414 13.3911 13 13.1978 13 13C13 12.7348 12.8946 12.4804 12.7071 12.2929C12.5196 12.1054 12.2652 12 12 12ZM12 6C11.7348 6 11.4804 6.10536 11.2929 6.29289C11.1054 6.48043 11 6.73478 11 7V10C11 10.2652 11.1054 10.5196 11.2929 10.7071C11.4804 10.8946 11.7348 11 12 11C12.2652 11 12.5196 10.8946 12.7071 10.7071C12.8946 10.5196 13 10.2652 13 10V7C13 6.73478 12.8946 6.48043 12.7071 6.29289C12.5196 6.10536 12.2652 6 12 6Z" fill="#CC0505" /> </div>
</svg> <div className='p-3 '>
</div> <h1 className='text-2xl font-bold text-red-600'>Error</h1>
<div className='p-3 '> <p className='pt-0 text-md text-red-600'>Something went wrong</p>
<h1 className='text-2xl font-bold text-red-600'>Error</h1> </div>
<p className='pt-0 text-md text-red-600'>Something went wrong</p>
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }

View file

@ -5,7 +5,7 @@ import { blackA, violet, mauve } from '@radix-ui/colors';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
const FormLayout = (props: any, onSubmit: any) => ( const FormLayout = (props: any, onSubmit: any) => (
<FormRoot onSubmit={props.onSubmit}> <FormRoot className='h-fit' onSubmit={props.onSubmit}>
{props.children} {props.children}
</FormRoot> </FormRoot>
); );

View file

@ -27,7 +27,7 @@ const Modal = (params: ModalParams) => (
<Dialog.Portal> <Dialog.Portal>
<DialogOverlay /> <DialogOverlay />
<DialogContent minHeight={params.minHeight}> <DialogContent className='overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full' minHeight={params.minHeight}>
<DialogTopBar className='-space-y-1'> <DialogTopBar className='-space-y-1'>
<DialogTitle>{params.dialogTitle}</DialogTitle> <DialogTitle>{params.dialogTitle}</DialogTitle>
<DialogDescription> <DialogDescription>
@ -118,7 +118,6 @@ const DialogContent = styled(Dialog.Content, {
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: '90vw', width: '90vw',
overflow: 'hidden',
maxHeight: '85vh', maxHeight: '85vh',
minHeight: '300px', minHeight: '300px',
maxWidth: '600px', maxWidth: '600px',

View file

@ -25,6 +25,7 @@
"@tiptap/react": "^2.0.0-beta.199", "@tiptap/react": "^2.0.0-beta.199",
"@tiptap/starter-kit": "^2.0.0-beta.199", "@tiptap/starter-kit": "^2.0.0-beta.199",
"avvvatars-react": "^0.4.2", "avvvatars-react": "^0.4.2",
"dayjs": "^1.11.10",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^10.16.1", "framer-motion": "^10.16.1",
"lowlight": "^3.0.0", "lowlight": "^3.0.0",
@ -4948,6 +4949,11 @@
"integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==", "integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==",
"peer": true "peer": true
}, },
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View file

@ -16,7 +16,7 @@
"@radix-ui/react-icons": "^1.1.1", "@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.5", "@radix-ui/react-tooltip": "^1.0.5",
"@sentry/nextjs": "^7.92.0", "@sentry/nextjs": "^7.93.0",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"@tiptap/extension-code-block-lowlight": "^2.1.11", "@tiptap/extension-code-block-lowlight": "^2.1.11",
"@tiptap/extension-collaboration": "^2.0.0-beta.199", "@tiptap/extension-collaboration": "^2.0.0-beta.199",
@ -26,6 +26,7 @@
"@tiptap/react": "^2.0.0-beta.199", "@tiptap/react": "^2.0.0-beta.199",
"@tiptap/starter-kit": "^2.0.0-beta.199", "@tiptap/starter-kit": "^2.0.0-beta.199",
"avvvatars-react": "^0.4.2", "avvvatars-react": "^0.4.2",
"dayjs": "^1.11.10",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^10.16.1", "framer-motion": "^10.16.1",
"lowlight": "^3.0.0", "lowlight": "^3.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View file

@ -4,27 +4,31 @@
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
Sentry.init({ if (process.env.NODE_ENV !== "development") {
dsn: "https://5a456d54654c494b9a416c19e3b94573@o4505007882436608.ingest.sentry.io/4505008095625216", const DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
// Adjust this value in production, or use tracesSampler for greater control Sentry.init({
tracesSampleRate: 1, dsn: DSN,
// Setting this option to true will print useful information to the console while you're setting up Sentry. // Adjust this value in production, or use tracesSampler for greater control
debug: false, tracesSampleRate: 1,
replaysOnErrorSampleRate: 1.0, // Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
// This sets the sample rate to be 10%. You may want this to be 100% while replaysOnErrorSampleRate: 1.0,
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// You can remove this option if you're not planning to use the Sentry Session Replay feature: // This sets the sample rate to be 10%. You may want this to be 100% while
integrations: [ // in development and sample at a lower rate in production
new Sentry.Replay({ replaysSessionSampleRate: 0.1,
// Additional Replay configuration goes in here, for example:
maskAllText: true, // You can remove this option if you're not planning to use the Sentry Session Replay feature:
blockAllMedia: true, integrations: [
}), new Sentry.Replay({
], // Additional Replay configuration goes in here, for example:
}); maskAllText: true,
blockAllMedia: true,
}),
],
});
}

View file

@ -1,16 +0,0 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://5a456d54654c494b9a416c19e3b94573@o4505007882436608.ingest.sentry.io/4505008095625216",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View file

@ -4,12 +4,10 @@
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
Sentry.init({ if (process.env.NODE_ENV !== "development") {
dsn: "https://5a456d54654c494b9a416c19e3b94573@o4505007882436608.ingest.sentry.io/4505008095625216", const DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
// Adjust this value in production, or use tracesSampler for greater control dsn: DSN,
tracesSampleRate: 1, tracesSampleRate: 1.0,
});
// Setting this option to true will print useful information to the console while you're setting up Sentry. }
debug: false,
});

View file

@ -1,5 +1,4 @@
import { getAPIUrl } from "@services/config/config"; import { getAPIUrl } from "@services/config/config";
import { NextApiRequestCookies } from "next/dist/server/api-utils";
interface LoginAndGetTokenResponse { interface LoginAndGetTokenResponse {
access_token: "string"; access_token: "string";
@ -83,7 +82,7 @@ export async function getUserSession(token: string): Promise<any> {
export async function getNewAccessTokenUsingRefreshToken(): Promise<any> { export async function getNewAccessTokenUsingRefreshToken(): Promise<any> {
const requestOptions: any = { const requestOptions: any = {
method: "POST", method: "GET",
redirect: "follow", redirect: "follow",
credentials: "include", credentials: "include",
}; };
@ -95,7 +94,7 @@ export async function getNewAccessTokenUsingRefreshToken(): Promise<any> {
export async function getNewAccessTokenUsingRefreshTokenServer(refresh_token_cookie: any): Promise<any> { export async function getNewAccessTokenUsingRefreshTokenServer(refresh_token_cookie: any): Promise<any> {
const requestOptions: any = { const requestOptions: any = {
method: "POST", method: "GET",
redirect: "follow", redirect: "follow",
headers: { headers: {
Cookie: `refresh_token_cookie=${refresh_token_cookie}`, Cookie: `refresh_token_cookie=${refresh_token_cookie}`,
@ -139,3 +138,18 @@ export async function signup(body: NewAccountBody): Promise<any> {
return res; return res;
} }
export async function signUpWithInviteCode(body: NewAccountBody,invite_code:string): Promise<any> {
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
const requestOptions: any = {
method: "POST",
headers: HeadersConfig,
body: JSON.stringify(body),
redirect: "follow",
};
const res = await fetch(`${getAPIUrl()}users/${body.org_id}/invite/${invite_code}`, requestOptions);
return res;
}

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "@services/config/config"; import { getAPIUrl } from "@services/config/config";
import { RequestBody, RequestBodyForm, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests"; import { RequestBody, RequestBodyForm, RequestBodyWithAuthHeader, errorHandling, getResponseMetadata } from "@services/utils/ts/requests";
/* /*
This file includes only POST, PUT, DELETE requests This file includes only POST, PUT, DELETE requests
@ -36,6 +36,14 @@ export async function getCourse(course_uuid: string, next: any) {
return res; return res;
} }
export async function updateCourseThumbnail(course_uuid: any, thumbnail: any) {
const formData = new FormData();
formData.append("thumbnail", thumbnail);
const result: any = await fetch(`${getAPIUrl()}courses/${course_uuid}/thumbnail`, RequestBodyForm("PUT", formData, null));
const res = await getResponseMetadata(result);
return res;
}
export async function createNewCourse(org_id: string, course_body: any, thumbnail: any) { export async function createNewCourse(org_id: string, course_body: any, thumbnail: any) {
// Send file thumbnail as form data // Send file thumbnail as form data
const formData = new FormData(); const formData = new FormData();
@ -45,7 +53,10 @@ export async function createNewCourse(org_id: string, course_body: any, thumbnai
formData.append("learnings", course_body.tags); formData.append("learnings", course_body.tags);
formData.append("tags", course_body.tags); formData.append("tags", course_body.tags);
formData.append("about", course_body.description); formData.append("about", course_body.description);
formData.append("thumbnail", thumbnail);
if (thumbnail) {
formData.append("thumbnail", thumbnail);
}
const result = await fetch(`${getAPIUrl()}courses/?org_id=${org_id}`, RequestBodyForm("POST", formData, null)); const result = await fetch(`${getAPIUrl()}courses/?org_id=${org_id}`, RequestBodyForm("POST", formData, null));
const res = await errorHandling(result); const res = await errorHandling(result);

View file

@ -9,38 +9,43 @@ function getMediaUrl() {
} }
} }
export function getCourseThumbnailMediaDirectory(orgId: string, courseId: string, fileId: string) { export function getCourseThumbnailMediaDirectory(orgUUID: string, courseId: string, fileId: string) {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/thumbnails/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/thumbnails/${fileId}`;
return uri; return uri;
} }
export function getActivityBlockMediaDirectory(orgId: string, courseId: string, activityId: string, blockId: any, fileId: any, type: string) { export function getUserAvatarMediaDirectory(userUUID: string, fileId: string) {
let uri = `${getMediaUrl()}content/users/${userUUID}/avatars/${fileId}`;
return uri;
}
export function getActivityBlockMediaDirectory(orgUUID: string, courseId: string, activityId: string, blockId: any, fileId: any, type: string) {
if (type == "pdfBlock") { if (type == "pdfBlock") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/dynamic/blocks/pdfBlock/${blockId}/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/dynamic/blocks/pdfBlock/${blockId}/${fileId}`;
return uri; return uri;
} }
if (type == "videoBlock") { if (type == "videoBlock") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/dynamic/blocks/videoBlock/${blockId}/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/dynamic/blocks/videoBlock/${blockId}/${fileId}`;
return uri; return uri;
} }
if (type == "imageBlock") { if (type == "imageBlock") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/dynamic/blocks/imageBlock/${blockId}/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/dynamic/blocks/imageBlock/${blockId}/${fileId}`;
return uri; return uri;
} }
} }
export function getActivityMediaDirectory(orgId: string, courseId: string, activityId: string, fileId: string, activityType: string) { export function getActivityMediaDirectory(orgUUID: string, courseId: string, activityId: string, fileId: string, activityType: string) {
if (activityType == "video") { if (activityType == "video") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/video/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/video/${fileId}`;
return uri; return uri;
} }
if (activityType == "documentpdf") { if (activityType == "documentpdf") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/documentpdf/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/documentpdf/${fileId}`;
return uri; return uri;
} }
} }
export function getOrgLogoMediaDirectory(orgId: string, fileId: string) { export function getOrgLogoMediaDirectory(orgUUID: string, fileId: string) {
let uri = `${getMediaUrl()}content/${orgId}/logos/${fileId}`; let uri = `${getMediaUrl()}content/orgs/${orgUUID}/logos/${fileId}`;
return uri; return uri;
} }

View file

@ -0,0 +1,26 @@
import { getAPIUrl } from "@services/config/config";
import { RequestBody, errorHandling, getResponseMetadata } from "@services/utils/ts/requests";
export async function createInviteCode(org_id: any) {
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/invites`, RequestBody("POST", null, null));
const res = await getResponseMetadata(result);
return res;
}
export async function deleteInviteCode(org_id: any, org_invite_code_uuid: string) {
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/invites/${org_invite_code_uuid}`, RequestBody("DELETE", null, null));
const res = await getResponseMetadata(result);
return res;
}
export async function changeSignupMechanism(org_id: any, signup_mechanism: string) {
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/signup_mechanism?signup_mechanism=${signup_mechanism}`, RequestBody("PUT", null, null));
const res = await getResponseMetadata(result);
return res;
}
export async function validateInviteCode(org_id: any, invite_code: string) {
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/invites/code/${invite_code}`, RequestBody("GET", null, null));
const res = await getResponseMetadata(result);
return res;
}

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "@services/config/config"; import { getAPIUrl } from "@services/config/config";
import { RequestBody, errorHandling } from "@services/utils/ts/requests"; import { RequestBody, errorHandling, getResponseMetadata } from "@services/utils/ts/requests";
/* /*
This file includes only POST, PUT, DELETE requests This file includes only POST, PUT, DELETE requests
@ -49,3 +49,15 @@ export function getOrganizationContextInfoNoAsync(org_slug: any, next: any) {
const result = fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null, next)); const result = fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null, next));
return result; return result;
} }
export async function updateUserRole(org_id: any, user_id: any, role_uuid: any) {
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/users/${user_id}/role/${role_uuid}`, RequestBody("PUT", null, null));
const res = await getResponseMetadata(result);
return res;
}
export async function removeUserFromOrg(org_id: any, user_id: any) {
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/users/${user_id}`, RequestBody("DELETE", null, null));
const res = await getResponseMetadata(result);
return res;
}

Some files were not shown because too many files have changed in this diff Show more