Added containers.
This commit is contained in:
parent
e1727caa5f
commit
527b010b24
src/snek
mapper
model
service
templates/settings/containers
view/settings
@ -10,10 +10,12 @@ from snek.mapper.user import UserMapper
|
|||||||
from snek.mapper.user_property import UserPropertyMapper
|
from snek.mapper.user_property import UserPropertyMapper
|
||||||
from snek.mapper.repository import RepositoryMapper
|
from snek.mapper.repository import RepositoryMapper
|
||||||
from snek.mapper.channel_attachment import ChannelAttachmentMapper
|
from snek.mapper.channel_attachment import ChannelAttachmentMapper
|
||||||
|
from snek.mapper.container import ContainerMapper
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
|
|
||||||
def get_mappers(app=None):
|
def get_mappers(app=None):
|
||||||
return Object(
|
return Object(
|
||||||
**{
|
**{
|
||||||
@ -27,6 +29,7 @@ def get_mappers(app=None):
|
|||||||
"user_property": UserPropertyMapper(app=app),
|
"user_property": UserPropertyMapper(app=app),
|
||||||
"repository": RepositoryMapper(app=app),
|
"repository": RepositoryMapper(app=app),
|
||||||
"channel_attachment": ChannelAttachmentMapper(app=app),
|
"channel_attachment": ChannelAttachmentMapper(app=app),
|
||||||
|
"container": ContainerMapper(app=app),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
6
src/snek/mapper/container.py
Normal file
6
src/snek/mapper/container.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from snek.model.container import Container
|
||||||
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
class ContainerMapper(BaseMapper):
|
||||||
|
model_class = Container
|
||||||
|
table_name = "container"
|
@ -12,6 +12,7 @@ from snek.model.user import UserModel
|
|||||||
from snek.model.user_property import UserPropertyModel
|
from snek.model.user_property import UserPropertyModel
|
||||||
from snek.model.repository import RepositoryModel
|
from snek.model.repository import RepositoryModel
|
||||||
from snek.model.channel_attachment import ChannelAttachmentModel
|
from snek.model.channel_attachment import ChannelAttachmentModel
|
||||||
|
from snek.model.container import Container
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ def get_models():
|
|||||||
"user_property": UserPropertyModel,
|
"user_property": UserPropertyModel,
|
||||||
"repository": RepositoryModel,
|
"repository": RepositoryModel,
|
||||||
"channel_attachment": ChannelAttachmentModel,
|
"channel_attachment": ChannelAttachmentModel,
|
||||||
|
"container": Container,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
10
src/snek/model/container.py
Normal file
10
src/snek/model/container.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
|
||||||
|
class Container(BaseModel):
|
||||||
|
id = ModelField(name="id", required=True, kind=str)
|
||||||
|
name = ModelField(name="name", required=True, kind=str)
|
||||||
|
status = ModelField(name="status", required=True, kind=str)
|
||||||
|
resources = ModelField(name="resources", required=False, kind=str)
|
||||||
|
user_uid = ModelField(name="user_uid", required=False, kind=str)
|
||||||
|
path = ModelField(name="path", required=False, kind=str)
|
||||||
|
readonly = ModelField(name="readonly", required=False, kind=bool, default=False)
|
@ -13,6 +13,7 @@ from snek.service.user_property import UserPropertyService
|
|||||||
from snek.service.util import UtilService
|
from snek.service.util import UtilService
|
||||||
from snek.service.repository import RepositoryService
|
from snek.service.repository import RepositoryService
|
||||||
from snek.service.channel_attachment import ChannelAttachmentService
|
from snek.service.channel_attachment import ChannelAttachmentService
|
||||||
|
from snek.service.container import ContainerService
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
from snek.service.db import DBService
|
from snek.service.db import DBService
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ def get_services(app):
|
|||||||
"repository": RepositoryService(app=app),
|
"repository": RepositoryService(app=app),
|
||||||
"db": DBService(app=app),
|
"db": DBService(app=app),
|
||||||
"channel_attachment": ChannelAttachmentService(app=app),
|
"channel_attachment": ChannelAttachmentService(app=app),
|
||||||
|
"container": ContainerService(app=app),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
29
src/snek/service/container.py
Normal file
29
src/snek/service/container.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
class ContainerService(BaseService):
|
||||||
|
mapper_name = "container"
|
||||||
|
|
||||||
|
async def create(self, id, name, status, resources=None, user_uid=None, path=None, readonly=False):
|
||||||
|
model = await self.new()
|
||||||
|
model["id"] = id
|
||||||
|
model["name"] = name
|
||||||
|
model["status"] = status
|
||||||
|
if resources:
|
||||||
|
model["resources"] = resources
|
||||||
|
if user_uid:
|
||||||
|
model["user_uid"] = user_uid
|
||||||
|
if path:
|
||||||
|
model["path"] = path
|
||||||
|
model["readonly"] = readonly
|
||||||
|
if await super().save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create container: {model.errors}")
|
||||||
|
|
||||||
|
async def get(self, id):
|
||||||
|
return await self.mapper.get(id)
|
||||||
|
|
||||||
|
async def update(self, model):
|
||||||
|
return await self.mapper.update(model)
|
||||||
|
|
||||||
|
async def delete(self, id):
|
||||||
|
return await self.mapper.delete(id)
|
39
src/snek/templates/settings/containers/create.html
Normal file
39
src/snek/templates/settings/containers/create.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% extends 'settings/index.html' %}
|
||||||
|
|
||||||
|
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Container</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'settings/containers/form.html' %}
|
||||||
|
<div class="container">
|
||||||
|
<form action="/settings/containers/create.html" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="name"><i class="fa-solid fa-box"></i> Name</label>
|
||||||
|
<input type="text" id="name" name="name" required placeholder="Container name">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="status"><i class="fa-solid fa-info-circle"></i> Status</label>
|
||||||
|
<input type="text" id="status" name="status" required placeholder="Container status">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="resources"><i class="fa-solid fa-memory"></i> Resources</label>
|
||||||
|
<input type="text" id="resources" name="resources" placeholder="Resource details">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="user_uid"><i class="fa-solid fa-user"></i> User UID</label>
|
||||||
|
<input type="text" id="user_uid" name="user_uid" placeholder="User UID">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="path"><i class="fa-solid fa-folder"></i> Path</label>
|
||||||
|
<input type="text" id="path" name="path" placeholder="Container path">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="readonly" value="1">
|
||||||
|
<i class="fa-solid fa-lock"></i> Readonly
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit"><i class="fa-solid fa-plus"></i> Create</button>
|
||||||
|
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
src/snek/templates/settings/containers/delete.html
Normal file
17
src/snek/templates/settings/containers/delete.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'settings/index.html' %}
|
||||||
|
|
||||||
|
{% block header_text %}<h1><i class="fa-solid fa-trash-can"></i> Delete Container</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="container">
|
||||||
|
<p>Are you sure you want to <strong>delete</strong> the following container?</p>
|
||||||
|
<div class="container-name"><i class="fa-solid fa-box"></i> {{ container.name }}</div>
|
||||||
|
<form method="post" style="margin-top:1.5rem;">
|
||||||
|
<input type="hidden" name="id" value="{{ container.id }}">
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit"><i class="fa-solid fa-trash"></i> Yes, delete</button>
|
||||||
|
<button type="button" onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i> Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
28
src/snek/templates/settings/containers/form.html
Normal file
28
src/snek/templates/settings/containers/form.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
div {
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 15px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
||||||
|
button {
|
||||||
|
background: #0d6efd; color: #fff;
|
||||||
|
border: none; border-radius: 5px; padding: 0.6rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.cancel {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container { max-width: 98vw; }
|
||||||
|
form { padding: 1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
96
src/snek/templates/settings/containers/index.html
Normal file
96
src/snek/templates/settings/containers/index.html
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{% extends 'settings/index.html' %}
|
||||||
|
|
||||||
|
{% block header_text %}<h1><i class="fa-solid fa-database"></i> Containers</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Containers - List</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.container-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.container-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.container-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
.container-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container-row { flex-direction: column; align-items: stretch; }
|
||||||
|
.actions { justify-content: flex-start; }
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
button, a.button {
|
||||||
|
background: #198754; color: #fff; border: none; border-radius: 5px;
|
||||||
|
padding: 0.4rem 0.8rem; text-decoration: none; cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.button.delete { background: #dc3545; }
|
||||||
|
.button.edit { background: #0d6efd; }
|
||||||
|
.button.clone { background: #6c757d; }
|
||||||
|
.button.browse { background: #ffc107; color: #212529; }
|
||||||
|
.button.create { background: #20c997; margin-left: 0.5rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="topbar">
|
||||||
|
<a class="button create" href="/settings/containers/create.html">
|
||||||
|
<i class="fa-solid fa-plus"></i> New Container
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<section class="container-list">
|
||||||
|
{% for container in containers %}
|
||||||
|
<div class="container-row">
|
||||||
|
<div class="container-info">
|
||||||
|
<span class="container-name"><i class="fa-solid fa-box"></i> {{ container.name }}</span>
|
||||||
|
<span title="Status"><i class="fa-solid fa-info-circle"></i> {{ container.status }}</span>
|
||||||
|
<span title="Readonly">
|
||||||
|
<i class="fa-solid {% if container.readonly %}fa-lock{% else %}fa-lock-open{% endif %}"></i>
|
||||||
|
{% if container.readonly %}Readonly{% else %}Writable{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="button edit" href="/settings/containers/container/{{ container.id }}/update.html">
|
||||||
|
<i class="fa-solid fa-pen"></i> Edit
|
||||||
|
</a>
|
||||||
|
<a class="button delete" href="/settings/containers/container/{{ container.id }}/delete.html">
|
||||||
|
<i class="fa-solid fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{% endblock %}
|
40
src/snek/templates/settings/containers/update.html
Normal file
40
src/snek/templates/settings/containers/update.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends "settings/index.html" %}
|
||||||
|
|
||||||
|
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Container</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "settings/containers/form.html" %}
|
||||||
|
<div class="container">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="id" value="{{ container.id }}">
|
||||||
|
<div>
|
||||||
|
<label for="name"><i class="fa-solid fa-box"></i> Name</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{ container.name }}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="status"><i class="fa-solid fa-info-circle"></i> Status</label>
|
||||||
|
<input type="text" id="status" name="status" value="{{ container.status }}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="resources"><i class="fa-solid fa-memory"></i> Resources</label>
|
||||||
|
<input type="text" id="resources" name="resources" value="{{ container.resources }}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="user_uid"><i class="fa-solid fa-user"></i> User UID</label>
|
||||||
|
<input type="text" id="user_uid" name="user_uid" value="{{ container.user_uid }}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="path"><i class="fa-solid fa-folder"></i> Path</label>
|
||||||
|
<input type="text" id="path" name="path" value="{{ container.path }}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="readonly" value="1" {% if container.readonly %}checked{% endif %}>
|
||||||
|
<i class="fa-solid fa-lock"></i> Readonly
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit"><i class="fa-solid fa-pen"></i> Update</button>
|
||||||
|
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
91
src/snek/view/settings/containers.py
Normal file
91
src/snek/view/settings/containers.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import asyncio
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from snek.system.view import BaseFormView
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
class ContainersIndexView(BaseFormView):
|
||||||
|
|
||||||
|
login_required = True
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
|
||||||
|
user_uid = self.session.get("uid")
|
||||||
|
|
||||||
|
containers = []
|
||||||
|
async for container in self.services.container.find(user_uid=user_uid):
|
||||||
|
containers.append(container.record)
|
||||||
|
|
||||||
|
user = await self.services.user.get(uid=self.session.get("uid"))
|
||||||
|
|
||||||
|
return await self.render_template("settings/containers/index.html", {"containers": containers, "user": user})
|
||||||
|
|
||||||
|
class ContainersCreateView(BaseFormView):
|
||||||
|
|
||||||
|
login_required = True
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
|
||||||
|
return await self.render_template("settings/containers/create.html")
|
||||||
|
|
||||||
|
async def post(self):
|
||||||
|
data = await self.request.post()
|
||||||
|
container = await self.services.container.create(
|
||||||
|
user_uid=self.session.get("uid"),
|
||||||
|
name=data['name'],
|
||||||
|
status=data['status'],
|
||||||
|
resources=data.get('resources', ''),
|
||||||
|
path=data.get('path', ''),
|
||||||
|
readonly=bool(data.get('readonly', False))
|
||||||
|
)
|
||||||
|
return web.HTTPFound("/settings/containers/index.html")
|
||||||
|
|
||||||
|
class ContainersUpdateView(BaseFormView):
|
||||||
|
|
||||||
|
login_required = True
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
|
||||||
|
container = await self.services.container.get(
|
||||||
|
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
|
||||||
|
)
|
||||||
|
if not container:
|
||||||
|
return web.HTTPNotFound()
|
||||||
|
return await self.render_template("settings/containers/update.html", {"container": container.record})
|
||||||
|
|
||||||
|
async def post(self):
|
||||||
|
data = await self.request.post()
|
||||||
|
container = await self.services.container.get(
|
||||||
|
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
|
||||||
|
)
|
||||||
|
container['status'] = data['status']
|
||||||
|
container['resources'] = data.get('resources', '')
|
||||||
|
container['path'] = data.get('path', '')
|
||||||
|
container['readonly'] = bool(data.get('readonly', False))
|
||||||
|
await self.services.container.save(container)
|
||||||
|
return web.HTTPFound("/settings/containers/index.html")
|
||||||
|
|
||||||
|
class ContainersDeleteView(BaseFormView):
|
||||||
|
|
||||||
|
login_required = True
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
|
||||||
|
container = await self.services.container.get(
|
||||||
|
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
|
||||||
|
)
|
||||||
|
if not container:
|
||||||
|
return web.HTTPNotFound()
|
||||||
|
|
||||||
|
return await self.render_template("settings/containers/delete.html", {"container": container.record})
|
||||||
|
|
||||||
|
async def post(self):
|
||||||
|
user_uid = self.session.get("uid")
|
||||||
|
uid = self.request.match_info["uid"]
|
||||||
|
container = await self.services.container.get(
|
||||||
|
user_uid=user_uid, uid=uid
|
||||||
|
)
|
||||||
|
if not container:
|
||||||
|
return web.HTTPNotFound()
|
||||||
|
await self.services.container.delete(user_uid=user_uid, uid=uid)
|
||||||
|
return web.HTTPFound("/settings/containers/index.html")
|
Loading…
Reference in New Issue
Block a user