Start working on perms, roles, namespaces
This commit is contained in:
parent
feb4366a54
commit
1204e50c52
14 changed files with 497 additions and 73 deletions
|
|
@ -5,4 +5,5 @@
|
||||||
- flask-login (for logging in)
|
- flask-login (for logging in)
|
||||||
- wtforms (for parsing form data)
|
- wtforms (for parsing form data)
|
||||||
- psycopg2 (for postgresql)
|
- psycopg2 (for postgresql)
|
||||||
- pyargon2 (for HashV1)
|
- pyargon2 (for HashV1)
|
||||||
|
- humanize (for generating human-readable timedeltas)
|
||||||
150
src/taskflower/auth/permission/__init__.py
Normal file
150
src/taskflower/auth/permission/__init__.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
|
||||||
|
from enum import IntFlag
|
||||||
|
|
||||||
|
from taskflower.db import db
|
||||||
|
from taskflower.db.model.namespace import Namespace
|
||||||
|
from taskflower.db.model.role import NamespaceRole, UserRole, UserToNamespaceRole
|
||||||
|
from taskflower.db.model.user import User
|
||||||
|
from taskflower.types.either import Either, Left, Right
|
||||||
|
|
||||||
|
|
||||||
|
class NamespacePermissionType(IntFlag):
|
||||||
|
NO_PERMS = 0
|
||||||
|
READ = (1 << 0)
|
||||||
|
ADMINISTRATE = (1 << 1)
|
||||||
|
CREATE_IN = (1 << 2)
|
||||||
|
COMPLETE_OWN = (1 << 3)
|
||||||
|
COMPLETE_ALL = (1 << 4)
|
||||||
|
UNCOMPLETE_OWN = (1 << 5)
|
||||||
|
UNCOMPLETE_ALL = (1 << 6)
|
||||||
|
EDIT_OWN = (1 << 7)
|
||||||
|
EDIT_ALL = (1 << 8)
|
||||||
|
DELETE_OWN = (1 << 9)
|
||||||
|
DELETE_ALL = (1 << 10)
|
||||||
|
EDIT_ROLES = (1 << 11)
|
||||||
|
|
||||||
|
class UserPermissionType(IntFlag):
|
||||||
|
NO_PERMS = 0
|
||||||
|
READ_PROFILE = (1 << 0)
|
||||||
|
EDIT_DISPLAY_NAME = (1 << 1)
|
||||||
|
EDIT_USERNAME = (1 << 2)
|
||||||
|
EDIT_PROFILE = (1 << 3)
|
||||||
|
SEE_ALL_TASKS_OF = (1 << 4)
|
||||||
|
COMPLETE_ALL_TASKS_OF = (1 << 5)
|
||||||
|
UNCOMPLETE_ALL_TASKS_OF = (1 << 6)
|
||||||
|
EDIT_ALL_TASKS_OF = (1 << 7)
|
||||||
|
DELETE_ALL_TASKS_OF = (1 << 8)
|
||||||
|
EDIT_ROLES = (1 << 9)
|
||||||
|
ADMINISTRATE = (1 << 10)
|
||||||
|
|
||||||
|
SELF_USER_PERMISSIONS = (
|
||||||
|
UserPermissionType.READ_PROFILE
|
||||||
|
| UserPermissionType.EDIT_DISPLAY_NAME
|
||||||
|
| UserPermissionType.EDIT_USERNAME
|
||||||
|
| UserPermissionType.EDIT_PROFILE
|
||||||
|
| UserPermissionType.SEE_ALL_TASKS_OF
|
||||||
|
| UserPermissionType.COMPLETE_ALL_TASKS_OF
|
||||||
|
| UserPermissionType.UNCOMPLETE_ALL_TASKS_OF
|
||||||
|
| UserPermissionType.EDIT_ALL_TASKS_OF
|
||||||
|
| UserPermissionType.DELETE_ALL_TASKS_OF
|
||||||
|
| UserPermissionType.EDIT_ROLES
|
||||||
|
| UserPermissionType.ADMINISTRATE
|
||||||
|
)
|
||||||
|
|
||||||
|
SELF_NAMESPACE_PERMISSIONS = (
|
||||||
|
NamespacePermissionType.READ
|
||||||
|
| NamespacePermissionType.ADMINISTRATE
|
||||||
|
| NamespacePermissionType.CREATE_IN
|
||||||
|
| NamespacePermissionType.COMPLETE_OWN
|
||||||
|
| NamespacePermissionType.COMPLETE_ALL
|
||||||
|
| NamespacePermissionType.UNCOMPLETE_OWN
|
||||||
|
| NamespacePermissionType.UNCOMPLETE_ALL
|
||||||
|
| NamespacePermissionType.EDIT_OWN
|
||||||
|
| NamespacePermissionType.EDIT_ALL
|
||||||
|
| NamespacePermissionType.DELETE_OWN
|
||||||
|
| NamespacePermissionType.DELETE_ALL
|
||||||
|
| NamespacePermissionType.EDIT_ROLES
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_user_role(user: User) -> Either[Exception, UserRole]:
|
||||||
|
try:
|
||||||
|
self_role = UserRole(
|
||||||
|
is_self=True, # pyright:ignore[reportCallIssue]
|
||||||
|
name='Self', # pyright:ignore[reportCallIssue]
|
||||||
|
description=f'Self-role for @user@{user.id}', # pyright:ignore[reportCallIssue]
|
||||||
|
permissions=int(SELF_USER_PERMISSIONS), # pyright:ignore[reportCallIssue]
|
||||||
|
perms_deny=0, # pyright:ignore[reportCallIssue]
|
||||||
|
priority=0, # pyright:ignore[reportCallIssue]
|
||||||
|
user=user.id # pyright:ignore[reportCallIssue]
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(self_role)
|
||||||
|
db.session.commit()
|
||||||
|
return Right(self_role)
|
||||||
|
except Exception as e:
|
||||||
|
return Left(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_user_namespace(user: User) -> Either[Exception, Namespace]:
|
||||||
|
try:
|
||||||
|
new_ns = Namespace(
|
||||||
|
name=f'@user@{user.id}\'s Namespace', # pyright:ignore[reportCallIssue]
|
||||||
|
description='Your default namespace!' # pyright:ignore[reportCallIssue]
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_ns)
|
||||||
|
db.session.commit()
|
||||||
|
return Right(new_ns)
|
||||||
|
except Exception as e:
|
||||||
|
return Left(e)
|
||||||
|
|
||||||
|
def _create_namespace_role(
|
||||||
|
namespace: Namespace
|
||||||
|
) -> Either[Exception, NamespaceRole]:
|
||||||
|
try:
|
||||||
|
new_ns_role = NamespaceRole(
|
||||||
|
name='Administrator', # pyright:ignore[reportCallIssue]
|
||||||
|
description='Default role for the namespace administrator.', # pyright:ignore[reportCallIssue]
|
||||||
|
permissions = SELF_NAMESPACE_PERMISSIONS, # pyright:ignore[reportCallIssue]
|
||||||
|
perms_deny = 0, # pyright:ignore[reportCallIssue]
|
||||||
|
priority = 0, # pyright:ignore[reportCallIssue]
|
||||||
|
namespace = namespace.id # pyright:ignore[reportCallIssue]
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_ns_role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return Right(new_ns_role)
|
||||||
|
except Exception as e:
|
||||||
|
return Left(e)
|
||||||
|
|
||||||
|
def _associate_namespace_role(
|
||||||
|
user: User,
|
||||||
|
namespace_role: NamespaceRole
|
||||||
|
) -> Either[Exception, UserToNamespaceRole]:
|
||||||
|
try:
|
||||||
|
associate_ns_role = UserToNamespaceRole(
|
||||||
|
user=user.id, # pyright:ignore[reportCallIssue]
|
||||||
|
role=namespace_role.id # pyright:ignore[reportCallIssue]
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(associate_ns_role)
|
||||||
|
db.session.commit()
|
||||||
|
return Right(associate_ns_role)
|
||||||
|
except Exception as e:
|
||||||
|
return Left(e)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_user_permissions(user: User):
|
||||||
|
return _create_user_role(user).flat_map(
|
||||||
|
lambda _: _gen_user_namespace(user)
|
||||||
|
).flat_map(
|
||||||
|
lambda user_ns: _create_namespace_role(user_ns)
|
||||||
|
).flat_map(
|
||||||
|
lambda user_ns_role: _associate_namespace_role(
|
||||||
|
user,
|
||||||
|
user_ns_role
|
||||||
|
)
|
||||||
|
).map(
|
||||||
|
lambda _: user
|
||||||
|
)
|
||||||
18
src/taskflower/auth/permission/lookups.py
Normal file
18
src/taskflower/auth/permission/lookups.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
from taskflower.db import db
|
||||||
|
from taskflower.db.model.namespace import Namespace
|
||||||
|
from taskflower.db.model.role import NamespaceRole, UserToNamespaceRole
|
||||||
|
from taskflower.db.model.user import User
|
||||||
|
|
||||||
|
def get_namespaces_for_user(user: User):
|
||||||
|
return db.session.query(
|
||||||
|
Namespace
|
||||||
|
).join(
|
||||||
|
NamespaceRole,
|
||||||
|
Namespace.id == NamespaceRole.namespace
|
||||||
|
).join(
|
||||||
|
UserToNamespaceRole,
|
||||||
|
NamespaceRole.id == UserToNamespaceRole.role
|
||||||
|
).filter(
|
||||||
|
UserToNamespaceRole.user == user.id
|
||||||
|
).all()
|
||||||
40
src/taskflower/auth/permission/resolve.py
Normal file
40
src/taskflower/auth/permission/resolve.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
|
||||||
|
from taskflower.auth.permission import NamespacePermissionType
|
||||||
|
from taskflower.db import db
|
||||||
|
from taskflower.db.model.namespace import Namespace
|
||||||
|
from taskflower.db.model.role import NamespaceRole, UserToNamespaceRole
|
||||||
|
from taskflower.db.model.user import User
|
||||||
|
|
||||||
|
def resolve_perms_on_namespace(
|
||||||
|
user: User,
|
||||||
|
namespace: Namespace
|
||||||
|
) -> NamespacePermissionType:
|
||||||
|
roles = db.session.query(
|
||||||
|
NamespaceRole
|
||||||
|
).join(
|
||||||
|
Namespace,
|
||||||
|
NamespaceRole.namespace == Namespace.id
|
||||||
|
).join(
|
||||||
|
UserToNamespaceRole,
|
||||||
|
NamespaceRole.id == UserToNamespaceRole.role
|
||||||
|
).join(
|
||||||
|
User,
|
||||||
|
User.id == UserToNamespaceRole.user
|
||||||
|
).filter(
|
||||||
|
User.id == user.id
|
||||||
|
).filter(
|
||||||
|
Namespace.id == namespace.id
|
||||||
|
).order_by(
|
||||||
|
NamespaceRole.priority.desc()
|
||||||
|
).all()
|
||||||
|
|
||||||
|
perms: NamespacePermissionType = NamespacePermissionType.NO_PERMS
|
||||||
|
|
||||||
|
for role in roles:
|
||||||
|
perms = (
|
||||||
|
(perms | role.permissions)
|
||||||
|
& ~(role.perms_deny)
|
||||||
|
)
|
||||||
|
|
||||||
|
return perms
|
||||||
22
src/taskflower/db/model/namespace.py
Normal file
22
src/taskflower/db/model/namespace.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from taskflower.db import db
|
||||||
|
|
||||||
|
|
||||||
|
class Namespace(db.Model):
|
||||||
|
__tablename__: str = 'namespace'
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256))
|
||||||
|
description: Mapped[str] = mapped_column(String)
|
||||||
|
created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class TaskToNamespace(db.Model):
|
||||||
|
__tablename__: str = 'task_to_namespace'
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id'))
|
||||||
|
task: Mapped[int] = mapped_column(Integer, ForeignKey('task.id'))
|
||||||
43
src/taskflower/db/model/role.py
Normal file
43
src/taskflower/db/model/role.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from taskflower.db import db
|
||||||
|
|
||||||
|
|
||||||
|
class NamespaceRole(db.Model):
|
||||||
|
__tablename__: str = 'namespace_role'
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256))
|
||||||
|
description: Mapped[str] = mapped_column(String)
|
||||||
|
permissions: Mapped[int] = mapped_column(Integer)
|
||||||
|
perms_deny: Mapped[int] = mapped_column(Integer)
|
||||||
|
priority: Mapped[int] = mapped_column(Integer)
|
||||||
|
created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id'))
|
||||||
|
|
||||||
|
class UserToNamespaceRole(db.Model):
|
||||||
|
__tablename__: str = 'user_to_namespace_role'
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
user: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
role: Mapped[int] = mapped_column(Integer, ForeignKey('namespace_role.id'))
|
||||||
|
|
||||||
|
class UserRole(db.Model):
|
||||||
|
__tablename__: str = 'user_role'
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
is_self: Mapped[bool] = mapped_column(Boolean, default=False) # Self-roles can't be assigned to anyone other than the user whose account they are associated with
|
||||||
|
name: Mapped[str] = mapped_column(String(256))
|
||||||
|
description: Mapped[str] = mapped_column(String)
|
||||||
|
permissions: Mapped[int] = mapped_column(Integer)
|
||||||
|
perms_deny: Mapped[int] = mapped_column(Integer)
|
||||||
|
priority: Mapped[int] = mapped_column(Integer)
|
||||||
|
created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
user: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
|
||||||
|
class UserToUserRole(db.Model):
|
||||||
|
__tablename__: str = 'user_to_user_role'
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
user: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
role: Mapped[int] = mapped_column(Integer, ForeignKey('user_role.id'))
|
||||||
|
|
@ -46,7 +46,7 @@ class Task(db.Model, APISerializable):
|
||||||
__tablename__: str = 'task'
|
__tablename__: str = 'task'
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String)
|
name: Mapped[str] = mapped_column(String(256))
|
||||||
description: Mapped[str] = mapped_column(String)
|
description: Mapped[str] = mapped_column(String)
|
||||||
due: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
due: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
|
||||||
61
src/taskflower/static/list-view.css
Normal file
61
src/taskflower/static/list-view.css
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
.list{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.list{
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list tr {
|
||||||
|
background-color: var(--table-row-bg-1);
|
||||||
|
color: var(--on-table-row);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list tr:nth-child(odd) {
|
||||||
|
background-color: var(--table-row-bg-2);
|
||||||
|
color: var(--on-table-row);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .task-header-row {
|
||||||
|
border-bottom: 8px solid var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list td, .list th{
|
||||||
|
border-right: 4px solid var(--bg);
|
||||||
|
border-left: 4px solid var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list td {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .detail-view {
|
||||||
|
display: none;
|
||||||
|
border-bottom: 4px solid var(--bg);
|
||||||
|
border-top: 1px solid var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .detail-view-elem {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .detail-view.shown {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .detail-view-elem .small-details {
|
||||||
|
font-size: small;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .detail-view-elem h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .detail-view-elem .main-description {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
15
src/taskflower/templates/namespace/_listentry.html
Normal file
15
src/taskflower/templates/namespace/_listentry.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% macro namespace_header() %}
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Link</th>
|
||||||
|
</tr>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro inline_namespace(ns) %}
|
||||||
|
<tr id={{ "row-"~ns.id }}>
|
||||||
|
<td id={{ "name-"~ns.id }}>{{ ns.name }}</td>
|
||||||
|
<td id={{ "desc-"~ns.id }}>{{ ns.description }}</td>
|
||||||
|
<td id={{ "link-"~ns.id }}><a href={{ url_for("web.namespace.get", id=ns.id) }}>GO ➪</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endmacro %}
|
||||||
27
src/taskflower/templates/namespace/list.html
Normal file
27
src/taskflower/templates/namespace/list.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "main.html" %}
|
||||||
|
{% from "namespace/_listentry.html" import inline_namespace, namespace_header %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block head_extras %}
|
||||||
|
<link rel="stylesheet" href={{ url_for("static", filename="list-view.css") }} />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}My Namespaces{% endblock %}
|
||||||
|
|
||||||
|
{% block main_content %}
|
||||||
|
<h1>My Namespaces</h1>
|
||||||
|
<table class="list">
|
||||||
|
<colgroup>
|
||||||
|
<col span="1"/>
|
||||||
|
<col span="1"/>
|
||||||
|
<col span="1"/>
|
||||||
|
</colgroup>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{{ namespace_header() }}
|
||||||
|
{% for ns in namespaces %}
|
||||||
|
{{ inline_namespace(ns) }}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
{% extends "main.html" %}
|
{% extends "main.html" %}
|
||||||
{% from "task/_shorttask.html" import inline_task, inline_task_header %}
|
{% from "task/_shorttask.html" import inline_task, inline_task_header %}
|
||||||
|
|
||||||
|
{% block head_extras %}
|
||||||
|
<link rel="stylesheet" href={{ url_for("static", filename="list-view.css") }} />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}My Tasks{% endblock %}
|
{% block title %}My Tasks{% endblock %}
|
||||||
|
|
||||||
{% block main_content %}
|
{% block main_content %}
|
||||||
<h1>My Tasks</h1>
|
<h1>My Tasks</h1>
|
||||||
<table class="task-list">
|
<table class="list">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col span="1" style="width: 0;"/>
|
<col span="1" style="width: 0;"/>
|
||||||
<col span="1"/>
|
<col span="1"/>
|
||||||
<col span="1" style="width: 20%;"/>
|
<col span="1" style="width: 20%;"/>
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
<tbody class="task-list">
|
<tbody class="list">
|
||||||
{{ inline_task_header() }}
|
{{ inline_task_header() }}
|
||||||
{% for task in tasks %}
|
{% for task in tasks %}
|
||||||
{{ inline_task(task) }}
|
{{ inline_task(task) }}
|
||||||
|
|
@ -21,77 +25,13 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.task-list{
|
.list .checkbox {
|
||||||
/* display: grid;
|
|
||||||
grid-template-columns: min-content 20% auto max-content; */
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.task-list{
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list tr {
|
|
||||||
background-color: var(--table-row-bg-1);
|
|
||||||
color: var(--on-table-row);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list tr:nth-child(odd) {
|
|
||||||
background-color: var(--table-row-bg-2);
|
|
||||||
color: var(--on-table-row);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list .task-header-row {
|
|
||||||
border-bottom: 8px solid var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * td, .task-list * th{
|
|
||||||
border-right: 4px solid var(--bg);
|
|
||||||
border-left: 4px solid var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * td {
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .checkbox {
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-list * .task-due {
|
.list .task-due {
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-list * p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .detail-view {
|
|
||||||
display: none;
|
|
||||||
border-bottom: 4px solid var(--bg);
|
|
||||||
border-top: 1px solid var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .detail-view-elem {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .detail-view.shown {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .detail-view-elem .small-details {
|
|
||||||
font-size: small;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .detail-view-elem h1 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list * .detail-view-elem .main-description {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from taskflower.web.auth import web_auth
|
||||||
|
from taskflower.web.namespace import web_namespace
|
||||||
from taskflower.web.task import web_tasks
|
from taskflower.web.task import web_tasks
|
||||||
from taskflower.web.user import web_user
|
from taskflower.web.user import web_user
|
||||||
from taskflower.web.auth import web_auth
|
|
||||||
|
|
||||||
web_base = Blueprint('web', __name__, url_prefix='/')
|
web_base = Blueprint('web', __name__, url_prefix='/')
|
||||||
|
|
||||||
web_base.register_blueprint(web_tasks)
|
web_base.register_blueprint(web_tasks)
|
||||||
web_base.register_blueprint(web_user)
|
web_base.register_blueprint(web_user)
|
||||||
web_base.register_blueprint(web_auth)
|
web_base.register_blueprint(web_auth)
|
||||||
|
web_base.register_blueprint(web_namespace)
|
||||||
102
src/taskflower/web/namespace/__init__.py
Normal file
102
src/taskflower/web/namespace/__init__.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from flask import Blueprint, redirect, render_template, url_for
|
||||||
|
from flask_login import current_user, login_required # pyright:ignore[reportUnknownVariableType,reportMissingTypeStubs]
|
||||||
|
|
||||||
|
from taskflower.auth.permission import NamespacePermissionType
|
||||||
|
from taskflower.auth.permission.lookups import get_namespaces_for_user
|
||||||
|
from taskflower.auth.permission.resolve import resolve_perms_on_namespace
|
||||||
|
from taskflower.db.model.namespace import Namespace
|
||||||
|
from taskflower.db.model.user import User
|
||||||
|
from taskflower.types.either import Either, Left, Right, gather_successes
|
||||||
|
|
||||||
|
|
||||||
|
web_namespace = Blueprint(
|
||||||
|
'namespace',
|
||||||
|
__name__,
|
||||||
|
'/templates',
|
||||||
|
url_prefix='/namespace'
|
||||||
|
)
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class NamespacePermsForUser:
|
||||||
|
read: bool
|
||||||
|
create_in: bool
|
||||||
|
complete_own: bool
|
||||||
|
complete_all: bool
|
||||||
|
uncomplete_own: bool
|
||||||
|
uncomplete_all: bool
|
||||||
|
edit_own: bool
|
||||||
|
edit_all: bool
|
||||||
|
delete_own: bool
|
||||||
|
delete_all: bool
|
||||||
|
edit_roles: bool
|
||||||
|
administrate: bool
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class NamespaceForUser():
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
perms: NamespacePermsForUser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_user(ns: Namespace, user: User) -> Either[Exception, 'NamespaceForUser']:
|
||||||
|
perms = resolve_perms_on_namespace(
|
||||||
|
user,
|
||||||
|
ns
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
NamespacePermissionType.READ in perms
|
||||||
|
or NamespacePermissionType.ADMINISTRATE in perms
|
||||||
|
):
|
||||||
|
return Left(KeyError('No such namespace or insufficient permissions.'))
|
||||||
|
|
||||||
|
return Right(
|
||||||
|
NamespaceForUser(
|
||||||
|
ns.id,
|
||||||
|
ns.name,
|
||||||
|
ns.description,
|
||||||
|
NamespacePermsForUser(
|
||||||
|
NamespacePermissionType.READ in perms,
|
||||||
|
NamespacePermissionType.CREATE_IN in perms,
|
||||||
|
NamespacePermissionType.COMPLETE_OWN in perms,
|
||||||
|
NamespacePermissionType.COMPLETE_ALL in perms,
|
||||||
|
NamespacePermissionType.UNCOMPLETE_OWN in perms,
|
||||||
|
NamespacePermissionType.UNCOMPLETE_ALL in perms,
|
||||||
|
NamespacePermissionType.EDIT_OWN in perms,
|
||||||
|
NamespacePermissionType.EDIT_ALL in perms,
|
||||||
|
NamespacePermissionType.DELETE_OWN in perms,
|
||||||
|
NamespacePermissionType.DELETE_ALL in perms,
|
||||||
|
NamespacePermissionType.EDIT_ROLES in perms,
|
||||||
|
NamespacePermissionType.ADMINISTRATE in perms
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@web_namespace.route('/')
|
||||||
|
@login_required
|
||||||
|
def all():
|
||||||
|
cur_usr: User = current_user # pyright:ignore[reportAssignmentType]
|
||||||
|
namespace_list: list[Namespace] = get_namespaces_for_user(cur_usr)
|
||||||
|
|
||||||
|
namespaces_parsed = gather_successes([
|
||||||
|
NamespaceForUser.from_user(
|
||||||
|
ns, cur_usr
|
||||||
|
)
|
||||||
|
for ns in namespace_list
|
||||||
|
])
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'namespace/list.html',
|
||||||
|
namespaces=namespaces_parsed
|
||||||
|
)
|
||||||
|
|
||||||
|
@web_namespace.route('/<int:id>')
|
||||||
|
@login_required
|
||||||
|
def get(id: int):
|
||||||
|
return redirect(url_for('web.namespace.all'))
|
||||||
|
|
@ -5,6 +5,7 @@ from wtforms.validators import DataRequired, EqualTo, Length
|
||||||
|
|
||||||
from taskflower.auth import password_breach_count
|
from taskflower.auth import password_breach_count
|
||||||
from taskflower.auth.hash import make_hash_v1
|
from taskflower.auth.hash import make_hash_v1
|
||||||
|
from taskflower.auth.permission import initialize_user_permissions
|
||||||
from taskflower.db import db
|
from taskflower.db import db
|
||||||
from taskflower.db.model.user import User
|
from taskflower.db.model.user import User
|
||||||
from taskflower.types import ann
|
from taskflower.types import ann
|
||||||
|
|
@ -254,10 +255,12 @@ def create_user_page():
|
||||||
if request.method == 'POST' and form_data.validate():
|
if request.method == 'POST' and form_data.validate():
|
||||||
return _user_from_form(form_data).flat_map(
|
return _user_from_form(form_data).flat_map(
|
||||||
lambda usr: _add_to_db(usr)
|
lambda usr: _add_to_db(usr)
|
||||||
|
).flat_map(
|
||||||
|
lambda usr: initialize_user_permissions(usr)
|
||||||
).side_effect(
|
).side_effect(
|
||||||
lambda usr: login_user(usr)
|
lambda usr: login_user(usr)
|
||||||
).and_then(
|
).and_then(
|
||||||
lambda usr: redirect(url_for('web.task.all_tasks_view')),
|
lambda usr: redirect(url_for('web.task.all')),
|
||||||
lambda exc: response_from_exception(exc)
|
lambda exc: response_from_exception(exc)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue