Update.
This commit is contained in:
parent
c56bf4fb49
commit
ee40c905d4
src/snek
@ -1,11 +1,14 @@
|
||||
import argparse
|
||||
|
||||
import uvloop
|
||||
from aiohttp import web
|
||||
|
||||
import asyncio
|
||||
from snek.app import Application
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
parser = argparse.ArgumentParser(description="Run the web application.")
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
|
@ -39,6 +39,10 @@ from snek.view.logout import LogoutView
|
||||
from snek.view.register import RegisterView
|
||||
from snek.view.rpc import RPCView
|
||||
from snek.view.search_user import SearchUserView
|
||||
from snek.view.settings.repositories import RepositoriesIndexView
|
||||
from snek.view.settings.repositories import RepositoriesCreateView
|
||||
from snek.view.settings.repositories import RepositoriesUpdateView
|
||||
from snek.view.settings.repositories import RepositoriesDeleteView
|
||||
from snek.view.settings.index import SettingsIndexView
|
||||
from snek.view.settings.profile import SettingsProfileView
|
||||
from snek.view.stats import StatsView
|
||||
@ -175,6 +179,10 @@ class Application(BaseApplication):
|
||||
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||
self.router.add_view("/stats.json", StatsView)
|
||||
self.router.add_view("/user/{user}.html", UserView)
|
||||
self.router.add_view("/settings/repositories/index.html", RepositoriesIndexView)
|
||||
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
|
||||
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
|
||||
self.router.add_view("/settings/repositories/respository/{name}/delete.html", RepositoriesDeleteView)
|
||||
self.webdav = WebdavApplication(self)
|
||||
self.add_subapp("/webdav", self.webdav)
|
||||
|
||||
|
@ -8,6 +8,7 @@ from snek.mapper.drive_item import DriveItemMapper
|
||||
from snek.mapper.notification import NotificationMapper
|
||||
from snek.mapper.user import UserMapper
|
||||
from snek.mapper.user_property import UserPropertyMapper
|
||||
from snek.mapper.repository import RepositoryMapper
|
||||
from snek.system.object import Object
|
||||
|
||||
|
||||
@ -23,6 +24,7 @@ def get_mappers(app=None):
|
||||
"drive_item": DriveItemMapper(app=app),
|
||||
"drive": DriveMapper(app=app),
|
||||
"user_property": UserPropertyMapper(app=app),
|
||||
"repository": RepositoryMapper(app=app),
|
||||
}
|
||||
)
|
||||
|
||||
|
7
src/snek/mapper/repository.py
Normal file
7
src/snek/mapper/repository.py
Normal file
@ -0,0 +1,7 @@
|
||||
from snek.model.repository import RepositoryModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
class RepositoryMapper(BaseMapper):
|
||||
model_class = RepositoryModel
|
||||
table_name = "repository"
|
@ -10,6 +10,7 @@ from snek.model.drive_item import DriveItemModel
|
||||
from snek.model.notification import NotificationModel
|
||||
from snek.model.user import UserModel
|
||||
from snek.model.user_property import UserPropertyModel
|
||||
from snek.model.repository import RepositoryModel
|
||||
from snek.system.object import Object
|
||||
|
||||
|
||||
@ -25,6 +26,7 @@ def get_models():
|
||||
"drive": DriveModel,
|
||||
"notification": NotificationModel,
|
||||
"user_property": UserPropertyModel,
|
||||
"repository": RepositoryModel,
|
||||
}
|
||||
)
|
||||
|
||||
|
14
src/snek/model/repository.py
Normal file
14
src/snek/model/repository.py
Normal file
@ -0,0 +1,14 @@
|
||||
from snek.model.user import UserModel
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
class RepositoryModel(BaseModel):
|
||||
|
||||
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||
|
||||
name = ModelField(name="name", required=True, kind=str)
|
||||
|
||||
is_private = ModelField(name="is_private", required=False, kind=bool)
|
||||
|
||||
|
||||
|
@ -11,6 +11,7 @@ from snek.service.socket import SocketService
|
||||
from snek.service.user import UserService
|
||||
from snek.service.user_property import UserPropertyService
|
||||
from snek.service.util import UtilService
|
||||
from snek.service.repository import RepositoryService
|
||||
from snek.system.object import Object
|
||||
|
||||
|
||||
@ -29,6 +30,7 @@ def get_services(app):
|
||||
"drive": DriveService(app=app),
|
||||
"drive_item": DriveItemService(app=app),
|
||||
"user_property": UserPropertyService(app=app),
|
||||
"repository": RepositoryService(app=app),
|
||||
}
|
||||
)
|
||||
|
||||
|
39
src/snek/service/repository.py
Normal file
39
src/snek/service/repository.py
Normal file
@ -0,0 +1,39 @@
|
||||
from snek.system.service import BaseService
|
||||
import asyncio
|
||||
|
||||
class RepositoryService(BaseService):
|
||||
mapper_name = "repository"
|
||||
|
||||
async def exists(self, user_uid, name, **kwargs):
|
||||
kwargs["user_uid"] = user_uid
|
||||
kwargs["name"] = name
|
||||
return await self.exists(**kwargs)
|
||||
|
||||
async def init(self, user_uid, name):
|
||||
repository_path = await self.services.user.get_repository_path(user_uid)
|
||||
if not repository_path.exists():
|
||||
repository_path.mkdir(parents=True)
|
||||
repository_path = repository_path.joinpath(name)
|
||||
command = ['git', 'init', '--bare', repository_path]
|
||||
process = await asyncio.subprocess.create_subprocess_exec(
|
||||
*command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
if process.returncode == 0:
|
||||
print(f"Bare Git repository created at: {repo_path}")
|
||||
else:
|
||||
print(f"Error creating repository: {stderr.decode().strip()}")
|
||||
|
||||
async def create(self, user_uid, name,is_private=False):
|
||||
if await self.exists(user_uid=user_uid, name=name):
|
||||
return False
|
||||
|
||||
|
||||
|
||||
model = await self.new()
|
||||
model["user_uid"] = user_uid
|
||||
model["name"] = name
|
||||
model["is_private"] = is_private
|
||||
return await self.save(model)
|
@ -42,6 +42,12 @@ class UserService(BaseService):
|
||||
def get_admin_uids(self):
|
||||
return self.mapper.get_admin_uids()
|
||||
|
||||
async def get_repository_path(self, user_uid):
|
||||
path = pathlib.Path(f"./drive/repositories/{user_uid}")
|
||||
if not path.exists():
|
||||
return None
|
||||
return path
|
||||
|
||||
async def get_static_path(self, user_uid):
|
||||
path = pathlib.Path(f"./drive/{user_uid}/snek/static")
|
||||
if not path.exists():
|
||||
|
59
src/snek/templates/settings/repositories/create.html
Normal file
59
src/snek/templates/settings/repositories/create.html
Normal file
@ -0,0 +1,59 @@
|
||||
{% extends 'settings/index.html' %}
|
||||
|
||||
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Repository</h1>{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||
<style>
|
||||
.container {
|
||||
div,input,label,button{
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
form {
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
||||
input[type="text"] {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc; border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
button, a.button {
|
||||
background: #198754; color: #fff; border: none; border-radius: 5px;
|
||||
padding: 0.1rem 0.8rem; text-decoration: none; cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<form action="/settings/repositories/create.html" method="post">
|
||||
<div>
|
||||
<label for="name"><i class="fa-solid fa-book"></i> Name</label>
|
||||
<input type="text" id="name" name="name" required placeholder="Repository name">
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="is_private" value="1">
|
||||
<i class="fa-solid fa-lock"></i> Private
|
||||
</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> Back</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
63
src/snek/templates/settings/repositories/delete.html
Normal file
63
src/snek/templates/settings/repositories/delete.html
Normal file
@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Delete Repository</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2rem; }
|
||||
.container { max-width: 400px; margin: 0 auto; }
|
||||
.confirm-box {
|
||||
background: #ffe5e8;
|
||||
border: 1.5px solid #dc3545;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.repo-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
color: #dc3545;
|
||||
}
|
||||
.actions {
|
||||
display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem;
|
||||
}
|
||||
button, a {
|
||||
background: #dc3545; color: #fff;
|
||||
border: none; border-radius: 5px; padding: 0.6rem 1.2rem;
|
||||
font-size: 1rem; cursor: pointer;
|
||||
display: flex; align-items: center; gap: 0.5rem; text-decoration: none; justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.cancel {
|
||||
background: #6c757d;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.container { max-width: 98vw; }
|
||||
.confirm-box { padding: 1rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1><i class="fa-solid fa-trash-can"></i> Delete Repository</h1>
|
||||
<div class="confirm-box">
|
||||
<div>
|
||||
<i class="fa-solid fa-triangle-exclamation" style="font-size:2rem; color:#dc3545;"></i>
|
||||
</div>
|
||||
<p>Are you sure you want to <strong>delete</strong> the following repository?</p>
|
||||
<div class="repo-name"><i class="fa-solid fa-book"></i> my-first-repo</div>
|
||||
<form action="/repositories/delete" method="post" style="margin-top:1.5rem;">
|
||||
<input type="hidden" name="id" value="1">
|
||||
<div class="actions">
|
||||
<button type="submit"><i class="fa-solid fa-trash"></i> Yes, delete</button>
|
||||
<a href="repositories.html" class="cancel"><i class="fa-solid fa-ban"></i> Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
106
src/snek/templates/settings/repositories/index.html
Normal file
106
src/snek/templates/settings/repositories/index.html
Normal file
@ -0,0 +1,106 @@
|
||||
{% extends 'settings/index.html' %}
|
||||
|
||||
{% block header_text %}<h1><i class="fa-solid fa-database"></i> Repositories</h1>{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Repositories - 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;
|
||||
}
|
||||
.repo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.repo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.repo-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
}
|
||||
.repo-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.repo-row { flex-direction: column; align-items: stretch; }
|
||||
.actions { justify-content: flex-start; }
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
/* justify-content: flex-end;*/
|
||||
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/repositories/create.html">
|
||||
<i class="fa-solid fa-plus"></i> New Repository
|
||||
</a>
|
||||
</div>
|
||||
<section class="repo-list">
|
||||
<!-- Example repository entries; replace with your templating/iteration -->
|
||||
{% for repo in repositories %}
|
||||
<div class="repo-row">
|
||||
<div class="repo-info">
|
||||
<span class="repo-name"><i class="fa-solid fa-book"></i> {{ repo.name }}</span>
|
||||
|
||||
<span title="Public">
|
||||
<i class="fa-solid {% if repo.is_private %}fa-lock{% else %}fa-lock-open{% endif %}"></i>
|
||||
{% if repo.is_private %}Private{% else %}Public{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="button browse" href="/repositories/{{ user.username }}/{{ repo.name }}" target="_blank">
|
||||
<i class="fa-solid fa-folder-open"></i> Browse
|
||||
</a>
|
||||
<a class="button clone" href="/repositories/{{ user.username }}/{{ repo.name }}/clone">
|
||||
<i class="fa-solid fa-code-branch"></i> Clone
|
||||
</a>
|
||||
<a class="button edit" href="/settings/repositories/repository/{{ repo.name }}/update.html">
|
||||
<i class="fa-solid fa-pen"></i> Edit
|
||||
</a>
|
||||
<a class="button delete" href="/settings/repositories/{{ repo.name }}/delete.html">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<!-- ... -->
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
45
src/snek/templates/settings/repositories/update.html
Normal file
45
src/snek/templates/settings/repositories/update.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends "settings/index.html" %}
|
||||
|
||||
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Repository</h1>{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<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;
|
||||
}
|
||||
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>
|
||||
<div class="container">
|
||||
<form method="post">
|
||||
<!-- Assume hidden id for backend use -->
|
||||
<input type="hidden" name="id" value="{{ repository.id }}">
|
||||
<div>
|
||||
<label for="name"><i class="fa-solid fa-book"></i> Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ repository.name }}" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="is_private" value="1" {% if repository.is_private %}checked{% endif %}>
|
||||
<i class="fa-solid fa-lock"></i> Private
|
||||
</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 %}
|
@ -3,7 +3,7 @@
|
||||
<h2>You</h2>
|
||||
<ul>
|
||||
<li><a class="no-select" href="/settings/profile.html">Profile</a></li>
|
||||
<li><a class="no-select" href="/settings/gists.html">Gists</a></li>
|
||||
<li><a class="no-select" href="/settings/repositories/index.html">Repositories</a></li>
|
||||
</ul>
|
||||
|
||||
</aside>
|
||||
|
68
src/snek/view/settings/repositories.py
Normal file
68
src/snek/view/settings/repositories.py
Normal file
@ -0,0 +1,68 @@
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
|
||||
from snek.system.view import BaseFormView
|
||||
import pathlib
|
||||
|
||||
class RepositoriesIndexView(BaseFormView):
|
||||
|
||||
login_required = True
|
||||
|
||||
async def get(self):
|
||||
|
||||
user_uid = self.session.get("uid")
|
||||
|
||||
repositories = []
|
||||
async for repository in self.services.repository.find(user_uid=user_uid):
|
||||
repositories.append(repository.record)
|
||||
|
||||
return await self.render_template("settings/repositories/index.html", {"repositories": repositories})
|
||||
|
||||
|
||||
|
||||
|
||||
class RepositoriesCreateView(BaseFormView):
|
||||
|
||||
login_required = True
|
||||
|
||||
async def get(self):
|
||||
|
||||
return await self.render_template("settings/repositories/create.html")
|
||||
|
||||
async def post(self):
|
||||
data = await self.request.post()
|
||||
repository = await self.services.repository.create(user_uid=self.session.get("uid"), name=data['name'], is_private=int(data.get('is_private',0)))
|
||||
return web.HTTPFound("/settings/repositories/index.html")
|
||||
|
||||
class RepositoriesUpdateView(BaseFormView):
|
||||
|
||||
login_required = True
|
||||
|
||||
async def get(self):
|
||||
|
||||
repository = await self.services.repository.get(
|
||||
user_uid=self.session.get("uid"), name=self.request.match_info["name"]
|
||||
)
|
||||
if not repository:
|
||||
return web.HTTPNotFound()
|
||||
return await self.render_template("settings/repositories/update.html", {"repository": repository.record})
|
||||
|
||||
async def post(self):
|
||||
data = await self.request.post()
|
||||
repository = await self.services.repository.get(
|
||||
user_uid=self.session.get("uid"), name=self.request.match_info["name"]
|
||||
)
|
||||
repository['is_private'] = int(data.get('is_private',0))
|
||||
await self.services.repository.save(repository)
|
||||
return web.HTTPFound("/settings/repositories/index.html")
|
||||
|
||||
class RepositoriesDeleteView(BaseFormView):
|
||||
|
||||
login_required = True
|
||||
|
||||
async def get(self):
|
||||
|
||||
return await self.render_template("settings/repositories/delete.html")
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user