From cd2fe6f4780c2c940e4ce957b56a414c5f9e89e9 Mon Sep 17 00:00:00 2001 From: Mikhail Sennikov <mifls@yandex.ru> Date: Sat, 16 Dec 2023 10:30:30 +0300 Subject: [PATCH] Groups sketch --- Backend/database/api/cam_groups.py | 16 ++++- Backend/database/api/cams.py | 7 +- Backend/database/api/roles.py | 2 +- Backend/database/models/cams.py | 2 +- Backend/database/repositories/cam_groups.py | 8 ++- Backend/database/repositories/cams.py | 2 +- Backend/main.py | 10 +-- Backend/requirements.txt | 1 + Frontend/package.json | 13 ++-- Frontend/src/App.vue | 9 ++- Frontend/src/components/CamGroup.vue | 72 +++++++++++++++++++++ Frontend/src/components/CameraPreview.vue | 3 +- Frontend/src/components/CamsList.vue | 61 +++++++++++++++++ Frontend/src/components/Gallery.vue | 49 +++----------- Frontend/src/components/GroupsList.vue | 52 +++++++++++++++ Frontend/vue.config.js | 22 +++++++ 16 files changed, 267 insertions(+), 62 deletions(-) create mode 100644 Frontend/src/components/CamGroup.vue create mode 100644 Frontend/src/components/CamsList.vue create mode 100644 Frontend/src/components/GroupsList.vue create mode 100644 Frontend/vue.config.js diff --git a/Backend/database/api/cam_groups.py b/Backend/database/api/cam_groups.py index 9fc1903..04735b4 100644 --- a/Backend/database/api/cam_groups.py +++ b/Backend/database/api/cam_groups.py @@ -6,6 +6,7 @@ from database import db_connection as db from database.repositories.cam_groups import GroupRepository from database.repositories.users import UserRepository from database.schemas.cam_groups import GroupFull, GroupBase +from database.schemas.cams import CamOut db_cam_groups_router = APIRouter() @@ -23,7 +24,7 @@ async def list_groups( @db_cam_groups_router.get("/{group_id}", response_model=GroupFull) -async def get_groups( +async def get_group( group_id: int, group_repo: GroupRepository = Depends(db.get_repository(GroupRepository)) ): @@ -76,6 +77,19 @@ async def delete_group( return group +@db_cam_groups_router.get("/{group_id}/cams", response_model=List[CamOut], tags=["Cams"]) +async def list_cams_in_group( + group_id: int, + group_repo: GroupRepository = Depends(db.get_repository(GroupRepository)) +): + group = await group_repo.get(group_id) + if group is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + return await group_repo.list_cams_in_group(group) + + @db_cam_groups_router.get("/user/{user_id}", response_model=List[GroupFull], tags=["Users"]) async def list_user_groups( user_id: int, diff --git a/Backend/database/api/cams.py b/Backend/database/api/cams.py index 344c345..a233a2a 100644 --- a/Backend/database/api/cams.py +++ b/Backend/database/api/cams.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, HTTPException, status, Depends from database import db_connection as db from database.repositories.cam_groups import GroupRepository from database.repositories.cams import CamRepository +from database.schemas.cam_groups import GroupFull from database.schemas.cams import CamOut, CamCreate, CamUpdate db_cams_router = APIRouter() @@ -76,7 +77,7 @@ async def delete_cam( return cam -@db_cams_router.get("/{cam_id}/groups", response_model=List[CamOut], tags=["Cam groups"]) +@db_cams_router.get("/{cam_id}/groups", response_model=List[GroupFull], tags=["Cam groups"]) async def list_cam_groups( cam_id: int, cam_repo: CamRepository = Depends(db.get_repository(CamRepository)) @@ -90,7 +91,7 @@ async def list_cam_groups( return groups -@db_cams_router.post("/{cam_id}/groups", response_model=CamOut, tags=["Cam groups"]) +@db_cams_router.post("/{cam_id}/groups", response_model=GroupFull, tags=["Cam groups"]) async def add_cam_group( cam_id: int, group_id: int, @@ -103,7 +104,7 @@ async def add_cam_group( return group -@db_cams_router.delete("/{cam_id}/groups", response_model=CamOut, tags=["Cam groups"]) +@db_cams_router.delete("/{cam_id}/groups", response_model=GroupFull, tags=["Cam groups"]) async def delete_cam_group( cam_id: int, group_id: int, diff --git a/Backend/database/api/roles.py b/Backend/database/api/roles.py index dbaf61e..0d7d175 100644 --- a/Backend/database/api/roles.py +++ b/Backend/database/api/roles.py @@ -22,7 +22,7 @@ async def list_roles( @db_roles_router.get("/{role_id}", response_model=RoleFull) -async def get_roles( +async def get_role( role_id: int, role_repo: RoleRepository = Depends(db.get_repository(RoleRepository)) ): diff --git a/Backend/database/models/cams.py b/Backend/database/models/cams.py index 9c53e42..11110b3 100644 --- a/Backend/database/models/cams.py +++ b/Backend/database/models/cams.py @@ -8,7 +8,7 @@ class Cam(Base): id: Column = Column(Integer, primary_key=True) # Will make SERIAL name: Column = Column(String, unique=True, nullable=False) host: Column = Column(String, nullable=False) - port: Column = Column(Integer, unique=True, nullable=False) + port: Column = Column(Integer, unique=False, nullable=False) username: Column = Column(String, nullable=False) password: Column = Column(String, nullable=False) diff --git a/Backend/database/repositories/cam_groups.py b/Backend/database/repositories/cam_groups.py index 1e3c63a..6a77c77 100644 --- a/Backend/database/repositories/cam_groups.py +++ b/Backend/database/repositories/cam_groups.py @@ -4,7 +4,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from database.models.auth import User -from database.models.cams import Group, UserInGroup +from database.models.cams import Group, UserInGroup, Cam, CamInGroup from database.repositories.base import Repository from database.schemas.cam_groups import GroupBase @@ -45,6 +45,12 @@ class GroupRepository(Repository): await self._session.commit() return group_db_obj + async def list_cams_in_group(self, group: Group) -> List[Cam]: + query = select(Cam).join(CamInGroup, CamInGroup.cam_id == Cam.id) + query = query.filter(CamInGroup.group_id == group.id) + cams_db_obj = await self._session.execute(query) + return [cam for cam, in cams_db_obj] + async def get_user_groups(self, user: User) -> List[Group]: query = select(Group).join(UserInGroup, UserInGroup.group_id == Group.id) query = query.filter(UserInGroup.user_id == user.id) diff --git a/Backend/database/repositories/cams.py b/Backend/database/repositories/cams.py index aea0125..a92387c 100644 --- a/Backend/database/repositories/cams.py +++ b/Backend/database/repositories/cams.py @@ -45,7 +45,7 @@ class CamRepository(Repository): return cam_db_obj async def get_cam_groups(self, cam: Cam) -> List[Group]: - query = select(Group).join(CamInGroup, CamInGroup.cam_id == Cam.id) + query = select(Group).join(CamInGroup, CamInGroup.cam_id == Group.id) query = query.filter(CamInGroup.cam_id == cam.id) groups_db_obj = await self._session.execute(query) return [group for group, in groups_db_obj] diff --git a/Backend/main.py b/Backend/main.py index 94f5da5..afcb42e 100644 --- a/Backend/main.py +++ b/Backend/main.py @@ -6,6 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.responses import FileResponse, RedirectResponse +from database import db_connection from database.api import db_router from onvif_proxy import cams_connection from onvif_proxy.onvif_proxy import onvif_router @@ -15,10 +16,10 @@ from proxy_switch.proxy_switch import switch_router app = FastAPI() -app.add_middleware( - CORSMiddleware, - allow_origins=['*'] -) +# app.add_middleware( +# CORSMiddleware, +# allow_origins=['*'] +# ) app.include_router( onvif_router, @@ -55,6 +56,7 @@ rtsp_task = None @app.on_event("startup") async def startup_event(): + await db_connection.create_models() global rtsp_task rtsp_task = asyncio.create_task(start_rtsp_server()) diff --git a/Backend/requirements.txt b/Backend/requirements.txt index 6727458..5c56635 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -1,6 +1,7 @@ aiohttp alembic asyncpg +bcrypt fastapi lxml onvif-zeep-async diff --git a/Frontend/package.json b/Frontend/package.json index 66ea8f8..3a9509f 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -8,19 +8,20 @@ "lint": "vue-cli-service lint" }, "dependencies": { - "@fortawesome/fontawesome-free": "^6.4.2", + "@fortawesome/fontawesome-free": "^6.5.1", "bootstrap": "^5.3.2", - "sass": "^1.69.3", + "sass": "^1.69.5", "sass-loader": "^13.3.2", - "vue": "^3.3.4" + "vue": "^3.3.11", + "axios": "^1.6.2" }, "devDependencies": { - "@babel/core": "^7.23.3", + "@babel/core": "^7.23.6", "@babel/eslint-parser": "^7.23.3", "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-service": "^5.0.8", - "eslint": "^8.54.0", - "eslint-plugin-vue": "^9.18.1" + "eslint": "^8.55.0", + "eslint-plugin-vue": "^9.19.2" }, "eslintConfig": { "root": true, diff --git a/Frontend/src/App.vue b/Frontend/src/App.vue index 35c3b8f..8a7d2b8 100644 --- a/Frontend/src/App.vue +++ b/Frontend/src/App.vue @@ -3,7 +3,8 @@ <NavBar id="navbar"/> <div class="content-wrapper"> <!-- <component :is="currentComponent"/>--> - <Gallery/> +<!-- <CamsList/>--> + <GroupsList/> </div> <Footer/> </div> @@ -11,13 +12,15 @@ <script> import NavBar from './components/NavBar.vue' -import Gallery from './components/Gallery.vue' +import CamsList from './components/CamsList.vue' import Footer from './components/Footer.vue' +import GroupsList from "@/components/GroupsList.vue"; export default { name: 'App', components: { - Gallery, + GroupsList, + CamsList, NavBar, Footer }, diff --git a/Frontend/src/components/CamGroup.vue b/Frontend/src/components/CamGroup.vue new file mode 100644 index 0000000..5fab73f --- /dev/null +++ b/Frontend/src/components/CamGroup.vue @@ -0,0 +1,72 @@ +<template> + <div class="group-container"> + <h2>{{group_name}}</h2> + <Gallery class="" + :cams="cams" + :active_cam_id="active_id"/> + </div> +</template> + +<script> +import axios from "axios"; +import Gallery from "@/components/Gallery.vue"; + +export default { + /* eslint-disable */ + name: 'CamGroup', + components: {Gallery}, + inject: ['Gallery'], + props: { + group_id: { + required: true + }, + group_name: { + required: false + }, + active_id: { + required: false + } + }, + mounted() { + setInterval(this.fetchCams, 3000) + }, + data() { + return { + cams: [] + }; + }, + created() { + this.fetchCams(); + }, + methods: { + fetchCams() { + axios.get(`/db/cam_groups/${this.group_id}/cams`, { + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => { + this.cams = response.data; + }) + .catch(error => { + console.error('Cams loading error:', error); + }); + } + } +} +</script> + +<style scoped> +.group-container { + border-bottom: 1px solid var(--light-color); +} + +h2 { + margin: 0.5rem; + padding: 0.5rem 0; + color: var(--white-color); + background-color: var(--light-color); + border: none; + border-radius: var(--bs-border-radius); +} +</style> diff --git a/Frontend/src/components/CameraPreview.vue b/Frontend/src/components/CameraPreview.vue index 825a59b..560d2d3 100644 --- a/Frontend/src/components/CameraPreview.vue +++ b/Frontend/src/components/CameraPreview.vue @@ -60,7 +60,8 @@ export default { chooseCamera() { fetch(`/switch/choose_camera/${this.camera_id}`, { method: 'POST', mode: 'no-cors' }) - this.Gallery.update_active_cam() + // TODO + // this.Gallery.update_active_cam() } } } diff --git a/Frontend/src/components/CamsList.vue b/Frontend/src/components/CamsList.vue new file mode 100644 index 0000000..caf7973 --- /dev/null +++ b/Frontend/src/components/CamsList.vue @@ -0,0 +1,61 @@ +<template> + <Gallery class="" + :cams="cams" + :active_cam_id="active_cam"/> +</template> + +<script> +import Gallery from "@/components/Gallery.vue"; + +export default { + /* eslint-disable */ + name: 'CamsList', + components: { + Gallery + }, + data() { + return { + cams: [], + active_cam: -1 + } + }, + methods: { + update_active_cam() { + fetch("/switch/active_cam", { method: 'GET' }) + .then( + response => { + if (response.ok) { + response.json().then(data => { + this.active_cam = data + }) + } + else if (response.status === 503) { + this.active_cam = -1 + } + }, + error => { + console.error("There was an error!", error); + }); + } + }, + mounted() { + fetch("/switch/cams", { method: 'GET' }) + .then( + response => { + if (response.ok) { + response.json().then(json => { + this.cams = json + }) + } + }, + error => { + console.error("There was an error!", error); + }); + setInterval(this.update_active_cam, 1000) + } +} +</script> + +<style scoped> + +</style> diff --git a/Frontend/src/components/Gallery.vue b/Frontend/src/components/Gallery.vue index 4c8601a..863c055 100644 --- a/Frontend/src/components/Gallery.vue +++ b/Frontend/src/components/Gallery.vue @@ -5,7 +5,7 @@ :camera_id="index" :camera_name="camera.name" :host="camera.host" - :active_id="active_cam"/> + :active_id="active_cam_id"/> </div> </div> </template> @@ -19,50 +19,18 @@ export default { components: { CameraPreview }, - data() { - return { - cams: [], - active_cam: -1 + props: { + cams: { + required: true + }, + active_cam_id: { + required: false } }, provide() { return { Gallery: this } - }, - methods: { - update_active_cam() { - fetch("/switch/active_cam", { method: 'GET' }) - .then( - response => { - if (response.ok) { - response.json().then(data => { - this.active_cam = data - }) - } - else if (response.status === 503) { - this.active_cam = -1 - } - }, - error => { - console.error("There was an error!", error); - }); - } - }, - mounted() { - fetch("/switch/cams", { method: 'GET' }) - .then( - response => { - if (response.ok) { - response.json().then(json => { - this.cams = json - }) - } - }, - error => { - console.error("There was an error!", error); - }); - setInterval(this.update_active_cam, 1000) } } </script> @@ -71,7 +39,8 @@ export default { .cams-container { display: grid; - padding: 3rem; + padding: 0; + margin: 0; justify-items: center; grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr)); grid-auto-rows: 19rem; diff --git a/Frontend/src/components/GroupsList.vue b/Frontend/src/components/GroupsList.vue new file mode 100644 index 0000000..9770e54 --- /dev/null +++ b/Frontend/src/components/GroupsList.vue @@ -0,0 +1,52 @@ +<template> + <div class="groups-container"> + <div v-for="group in groups"> + <CamGroup :group_id="group.id" + :group_name="group.name"/> + </div> + </div> +</template> + +<script> +import CamGroup from "@/components/CamGroup.vue"; + +export default { + /* eslint-disable */ + name: 'GroupsList', + components: { + CamGroup + }, + data() { + return { + groups: [], + active_group: -1 + } + }, + methods: { + update_groups() { + fetch("/db/cam_groups/list", { method: 'GET' }) + .then( + response => { + if (response.ok) { + response.json().then(json => { + this.groups = json + }) + } + }, + error => { + console.error("There was an error!", error); + }); + } + }, + mounted() { + this.update_groups() + setInterval(this.update_groups, 1000) + } +} +</script> + +<style scoped> +.groups-container { + margin: 1rem 3rem; +} +</style> diff --git a/Frontend/vue.config.js b/Frontend/vue.config.js new file mode 100644 index 0000000..cbc1709 --- /dev/null +++ b/Frontend/vue.config.js @@ -0,0 +1,22 @@ +module.exports = { + pluginOptions: { + i18n: { + locale: 'ru', + fallbackLocale: 'en', + localeDir: 'locales', + enableInSFC: true + } + }, + devServer: { + proxy: { + '^/assets': { + target: `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 8080}`, + changeOrigin: true + }, + '^/(?!ws)': { + target: 'http://localhost:9000', + changeOrigin: true + } + } + } +} \ No newline at end of file -- GitLab