Overhaul default access lists (web)
The changes are: - Introduce a visibility column, similar to git.sr.ht et al - Consolodate the default access columns into one
This commit is contained in:
parent
4521acc3ca
commit
d2f57b98c1
|
@ -1,14 +1,6 @@
|
|||
from srht.oauth import current_user
|
||||
from todosrht.types import User, Tracker, Ticket
|
||||
from todosrht.types import TicketAccess, UserAccess
|
||||
|
||||
def _get_permissions(tracker, ticket, name):
|
||||
"""
|
||||
Return ticket permissions of given name, fall back to tracker defaults.
|
||||
"""
|
||||
if ticket and getattr(ticket, f"{name}_perms"):
|
||||
return getattr(ticket, f"{name}_perms")
|
||||
return getattr(tracker, f"default_{name}_perms")
|
||||
from todosrht.types import User, Tracker, Ticket, Visibility
|
||||
from todosrht.types import TicketAccess, UserAccess, Participant
|
||||
|
||||
# TODO: get_access for any participant
|
||||
def get_access(tracker, ticket, user=None):
|
||||
|
@ -16,23 +8,22 @@ def get_access(tracker, ticket, user=None):
|
|||
|
||||
# Anonymous
|
||||
if not user:
|
||||
return _get_permissions(tracker, ticket, "anonymous")
|
||||
if tracker.visibility == Visibility.PRIVATE:
|
||||
return TicketAccess.none
|
||||
return tracker.default_access
|
||||
|
||||
# Owner
|
||||
if user.id == tracker.owner_id:
|
||||
return TicketAccess.all
|
||||
|
||||
# Per-user access specified
|
||||
# ACL entry?
|
||||
user_access = UserAccess.query.filter_by(tracker=tracker, user=user).first()
|
||||
if user_access:
|
||||
return user_access.permissions
|
||||
|
||||
# Submitter
|
||||
if ticket and user.id == ticket.submitter.user_id:
|
||||
return _get_permissions(tracker, ticket, "submitter")
|
||||
|
||||
# Any logged in user
|
||||
return _get_permissions(tracker, ticket, "user")
|
||||
if tracker.visibility == Visibility.PRIVATE:
|
||||
return TicketAccess.none
|
||||
return tracker.default_access
|
||||
|
||||
|
||||
def get_tracker(owner, name, with_for_update=False, user=None):
|
||||
|
@ -57,18 +48,19 @@ def get_tracker(owner, name, with_for_update=False, user=None):
|
|||
tracker = tracker.one_or_none()
|
||||
if not tracker:
|
||||
return None, None
|
||||
access = get_access(tracker, None, user=user)
|
||||
if access:
|
||||
return tracker, access
|
||||
return None, None
|
||||
return tracker, get_access(tracker, None, user=user)
|
||||
|
||||
def get_ticket(tracker, ticket_id, user=None):
|
||||
user = user or current_user
|
||||
ticket = (Ticket.query
|
||||
.join(Participant)
|
||||
.filter(Ticket.scoped_id == ticket_id)
|
||||
.filter(Ticket.tracker_id == tracker.id)).one_or_none()
|
||||
if not ticket:
|
||||
return None, None
|
||||
access = get_access(tracker, ticket, user=user)
|
||||
if user and user.id == ticket.submitter.user_id:
|
||||
access |= TicketAccess.browse
|
||||
if not TicketAccess.browse in access:
|
||||
return None, None
|
||||
return ticket, access
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
"""Unify default ACLs
|
||||
|
||||
Revision ID: 368579bcc610
|
||||
Revises: 9fca56774794
|
||||
Create Date: 2021-09-22 12:08:54.542597
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '368579bcc610'
|
||||
down_revision = '9fca56774794'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""
|
||||
ALTER TABLE tracker
|
||||
DROP COLUMN default_user_perms,
|
||||
DROP COLUMN default_submitter_perms,
|
||||
DROP COLUMN default_committer_perms;
|
||||
|
||||
ALTER TABLE tracker
|
||||
RENAME COLUMN default_anonymous_perms TO default_access;
|
||||
|
||||
ALTER TABLE ticket
|
||||
DROP COLUMN user_perms,
|
||||
DROP COLUMN submitter_perms,
|
||||
DROP COLUMN committer_perms,
|
||||
DROP COLUMN anonymous_perms;
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
ALTER TABLE tracker
|
||||
ADD COLUMN default_user_perms integer,
|
||||
ADD COLUMN default_committer_perms integer,
|
||||
ADD COLUMN default_submitter_perms integer;
|
||||
|
||||
ALTER TABLE tracker
|
||||
RENAME COLUMN default_access TO default_anonymous_perms;
|
||||
|
||||
UPDATE tracker
|
||||
SET
|
||||
default_user_perms = default_anonymous_perms,
|
||||
default_committer_perms = default_anonymous_perms,
|
||||
default_submitter_perms = default_anonymous_perms;
|
||||
|
||||
ALTER TABLE tracker
|
||||
ALTER COLUMN default_user_perms SET NOT NULL,
|
||||
ALTER COLUMN default_committer_perms SET NOT NULL,
|
||||
ALTER COLUMN default_submitter_perms SET NOT NULL;
|
||||
|
||||
ALTER TABLE ticket
|
||||
ADD COLUMN user_perms integer,
|
||||
ADD COLUMN committer_perms integer,
|
||||
ADD COLUMN submitter_perms integer,
|
||||
ADD COLUMN anonymous_perms integer;
|
||||
""")
|
|
@ -0,0 +1,43 @@
|
|||
"""Add visibility to trackers
|
||||
|
||||
Revision ID: 9fca56774794
|
||||
Revises: 6099fe670392
|
||||
Create Date: 2021-09-22 11:19:24.886544
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9fca56774794'
|
||||
down_revision = '6099fe670392'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""
|
||||
CREATE TYPE visibility AS ENUM (
|
||||
'PUBLIC', 'UNLISTED', 'PRIVATE'
|
||||
);
|
||||
|
||||
ALTER TABLE tracker
|
||||
ADD COLUMN visibility visibility;
|
||||
|
||||
UPDATE tracker
|
||||
SET visibility =
|
||||
CASE WHEN default_anonymous_perms & 1 > 0
|
||||
THEN 'PUBLIC'::visibility
|
||||
ELSE 'PRIVATE'::visibility
|
||||
END;
|
||||
|
||||
ALTER TABLE tracker
|
||||
ALTER COLUMN visibility
|
||||
SET NOT NULL;
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
ALTER TABLE tracker DROP COLUMN visibility;
|
||||
DROP TYPE visibility;
|
||||
""")
|
|
@ -1,7 +1,7 @@
|
|||
from flask import Blueprint, render_template, request, abort, redirect, url_for
|
||||
from todosrht.access import get_tracker, get_access
|
||||
from todosrht.tickets import get_participant_for_user
|
||||
from todosrht.types import Tracker, Ticket, TicketAccess
|
||||
from todosrht.types import Tracker, Ticket, TicketAccess, Visibility
|
||||
from todosrht.types import Event, EventNotification, EventType
|
||||
from todosrht.types import User, Participant
|
||||
from srht.config import cfg
|
||||
|
@ -17,39 +17,10 @@ def filter_authorized_events(events):
|
|||
events = (events
|
||||
.join(Ticket, Ticket.id == Event.ticket_id)
|
||||
.join(Tracker, Tracker.id == Ticket.tracker_id))
|
||||
if current_user:
|
||||
participant = get_participant_for_user(current_user)
|
||||
events = (events.filter(
|
||||
or_(
|
||||
and_(
|
||||
Ticket.submitter_perms != None,
|
||||
Ticket.submitter_id == participant.id,
|
||||
Ticket.submitter_perms.op('&')(TicketAccess.browse) > 0),
|
||||
and_(
|
||||
Ticket.user_perms != None,
|
||||
Ticket.user_perms.op('&')(TicketAccess.browse) > 0),
|
||||
and_(
|
||||
Ticket.anonymous_perms != None,
|
||||
Ticket.anonymous_perms.op('&')(TicketAccess.browse) > 0),
|
||||
and_(
|
||||
Ticket.submitter_perms == None,
|
||||
Ticket.submitter_id == participant.id,
|
||||
Tracker.default_submitter_perms.op('&')(TicketAccess.browse) > 0),
|
||||
and_(
|
||||
Ticket.user_perms == None,
|
||||
Tracker.default_user_perms.op('&')(TicketAccess.browse) > 0),
|
||||
and_(
|
||||
Ticket.anonymous_perms == None,
|
||||
Tracker.default_anonymous_perms.op('&')(TicketAccess.browse) > 0))))
|
||||
else:
|
||||
events = (events.filter(
|
||||
or_(
|
||||
and_(
|
||||
Ticket.anonymous_perms != None,
|
||||
Ticket.anonymous_perms.op('&')(TicketAccess.browse) > 0),
|
||||
and_(
|
||||
Ticket.anonymous_perms == None,
|
||||
Tracker.default_anonymous_perms.op('&')(TicketAccess.browse) > 0))))
|
||||
# TODO: Filter based on user ACLs?
|
||||
events = (events.filter(and_(
|
||||
Tracker.visibility == Visibility.PUBLIC,
|
||||
Tracker.default_access.op('&')(TicketAccess.browse) > 0)))
|
||||
return events
|
||||
|
||||
@html.route("/")
|
||||
|
@ -97,12 +68,9 @@ def user_GET(username):
|
|||
abort(404)
|
||||
|
||||
trackers = Tracker.query.filter(Tracker.owner_id == user.id)
|
||||
if current_user and current_user != user:
|
||||
trackers = trackers.filter(Tracker.default_user_perms
|
||||
.op('&')(TicketAccess.browse) > 0)
|
||||
elif not current_user:
|
||||
trackers = trackers.filter(Tracker.default_anonymous_perms
|
||||
.op('&')(TicketAccess.browse) > 0)
|
||||
if not current_user or user.id != current_user.id:
|
||||
trackers = trackers.filter(Tracker.visibility == Visibility.PUBLIC)
|
||||
|
||||
limit_trackers = 10
|
||||
total_trackers = trackers.count()
|
||||
trackers = (trackers
|
||||
|
@ -134,12 +102,8 @@ def trackers_for_user(username):
|
|||
abort(404)
|
||||
|
||||
trackers = Tracker.query.filter(Tracker.owner_id == user.id)
|
||||
if current_user and current_user != user:
|
||||
trackers = trackers.filter(Tracker.default_user_perms
|
||||
.op('&')(TicketAccess.browse) > 0)
|
||||
elif not current_user:
|
||||
trackers = trackers.filter(Tracker.default_anonymous_perms
|
||||
.op('&')(TicketAccess.browse) > 0)
|
||||
if not current_user or user.id != current_user.id:
|
||||
trackers = trackers.filter(Tracker.visibility == Visibility.PUBLIC)
|
||||
|
||||
search = request.args.get("search")
|
||||
if search:
|
||||
|
|
|
@ -13,7 +13,7 @@ from srht.validation import Validation
|
|||
from tempfile import NamedTemporaryFile
|
||||
from todosrht.access import get_tracker
|
||||
from todosrht.trackers import get_recent_users
|
||||
from todosrht.types import Event, EventType, Ticket, TicketAccess
|
||||
from todosrht.types import Event, EventType, Ticket, TicketAccess, Visibility
|
||||
from todosrht.types import ParticipantType, UserAccess, User
|
||||
from todosrht.urls import tracker_url
|
||||
from todosrht.webhooks import UserWebhook
|
||||
|
@ -64,6 +64,7 @@ def details_POST(owner, name):
|
|||
|
||||
valid = Validation(request)
|
||||
desc = valid.optional("tracker_desc", default=tracker.description)
|
||||
vis = valid.require("visibility", cls=Visibility)
|
||||
valid.expect(not desc or len(desc) < 4096,
|
||||
"Must be less than 4096 characters",
|
||||
field="tracker_desc")
|
||||
|
@ -72,6 +73,7 @@ def details_POST(owner, name):
|
|||
tracker=tracker, **valid.kwargs), 400
|
||||
|
||||
tracker.description = desc
|
||||
tracker.visibility = vis
|
||||
|
||||
UserWebhook.deliver(UserWebhook.Events.tracker_update,
|
||||
tracker.to_dict(),
|
||||
|
@ -108,16 +110,12 @@ def access_POST(owner, name):
|
|||
abort(403)
|
||||
|
||||
valid = Validation(request)
|
||||
perm_anon = parse_html_perms('anon', valid)
|
||||
perm_user = parse_html_perms('user', valid)
|
||||
perm_submit = parse_html_perms('submit', valid)
|
||||
access = parse_html_perms('default', valid)
|
||||
|
||||
if not valid.ok:
|
||||
return render_tracker_access(tracker, **valid.kwargs), 400
|
||||
|
||||
tracker.default_anonymous_perms = perm_anon
|
||||
tracker.default_user_perms = perm_user
|
||||
tracker.default_submitter_perms = perm_submit
|
||||
tracker.default_access = access
|
||||
|
||||
UserWebhook.deliver(UserWebhook.Events.tracker_update,
|
||||
tracker.to_dict(),
|
||||
|
|
|
@ -85,7 +85,15 @@ def return_tracker(tracker, access, **kwargs):
|
|||
f"{posting_domain}?subject={subj}&body=" + \
|
||||
quote(tracker_subscribe_body.format(tracker_ref=tracker.ref()))
|
||||
|
||||
tickets = Ticket.query.filter(Ticket.tracker_id == tracker.id)
|
||||
if TicketAccess.browse in access:
|
||||
tickets = Ticket.query.filter(Ticket.tracker_id == tracker.id)
|
||||
elif current_user:
|
||||
tickets = (Ticket.query
|
||||
.join(Participant, Participant.user_id == current_user.id)
|
||||
.filter(Ticket.tracker_id == tracker.id)
|
||||
.filter(Ticket.submitter_id == Participant.id))
|
||||
else:
|
||||
tickets = Ticket.query.filter("false")
|
||||
|
||||
try:
|
||||
terms = request.args.get("search")
|
||||
|
|
|
@ -41,44 +41,22 @@
|
|||
<div class="col-md-12">
|
||||
<form method="POST">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group {{valid.cls("tracker_any_access")}}">
|
||||
<div class="form-group {{valid.cls("tracker_default_access")}}">
|
||||
<p>
|
||||
These permissions allow you to control what kinds of users are able
|
||||
to do what sorts of activities on your tracker.
|
||||
</p>
|
||||
<div class="event-list">
|
||||
<div class="event">
|
||||
<h4>Anonymous Permissions</h4>
|
||||
<h4>Default Permissions</h4>
|
||||
<p>
|
||||
Permissions granted to anyone who visits this tracker, logged
|
||||
in or otherwise.
|
||||
These permissions are used for anyone who does not have a more
|
||||
specific access configuration.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, tracker.default_anonymous_perms, "anon") }}
|
||||
{{ perm_checkbox(a, tracker.default_access, "default") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("tracker_anon_access") }}
|
||||
</div>
|
||||
<div class="event">
|
||||
<h4>Submitter Permissions</h4>
|
||||
<p>
|
||||
Permissions granted to the ticket submitter on the tickets they
|
||||
submit.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, tracker.default_submitter_perms, "submit") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("tracker_submit_access") }}
|
||||
</div>
|
||||
<div class="event">
|
||||
<h4>Account Holder Permissions</h4>
|
||||
<p>
|
||||
Permissions granted to any logged-in {{cfg("sr.ht",
|
||||
"site-name")}} user.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, tracker.default_user_perms, "user") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("tracker_user_access") }}
|
||||
{{ valid.summary("tracker_default_access") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,53 +8,101 @@
|
|||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>Create new tracker</h2>
|
||||
<form method="POST" action="/tracker/create">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control {{valid.cls("name")}}"
|
||||
value="{{ name or "" }}"
|
||||
aria-describedby="name-help" />
|
||||
{{valid.summary("name")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control {{valid.cls("description")}}"
|
||||
value="{{ description or "" }}"
|
||||
rows="5"
|
||||
aria-describedby="description-help">{{description or ""}}</textarea>
|
||||
<p id="description-help" class="form-text text-muted">
|
||||
Markdown supported
|
||||
</p>
|
||||
{{valid.summary("description")}}
|
||||
</div>
|
||||
{{valid.summary()}}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
name="create"
|
||||
>
|
||||
Create tracker {{icon("caret-right")}}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-default"
|
||||
name="create-configure"
|
||||
>
|
||||
Create & configure {{icon("caret-right")}}
|
||||
</button>
|
||||
</form>
|
||||
<form class="row" method="POST" action="/tracker/create">
|
||||
{{csrf_token()}}
|
||||
<div class="col-md-12">
|
||||
<h3>Create new tracker</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control {{valid.cls("name")}}"
|
||||
value="{{ name or "" }}"
|
||||
aria-describedby="name-help" />
|
||||
{{valid.summary("name")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control {{valid.cls("description")}}"
|
||||
value="{{ description or "" }}"
|
||||
rows="5"
|
||||
aria-describedby="description-help">{{description or ""}}</textarea>
|
||||
<p id="description-help" class="form-text text-muted">
|
||||
Markdown supported
|
||||
</p>
|
||||
{{valid.summary("description")}}
|
||||
</div>
|
||||
{{valid.summary()}}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
name="create"
|
||||
>
|
||||
Create tracker {{icon("caret-right")}}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-default"
|
||||
name="create-configure"
|
||||
>
|
||||
Create & configure {{icon("caret-right")}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<fieldset class="form-group">
|
||||
<legend>Tracker Visibility</legend>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PUBLIC"
|
||||
checked> Public
|
||||
<small id="visibility-public-help" class="form-text text-muted">
|
||||
Shown on your profile page
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Visible to anyone with the link, but not shown on your profile"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="UNLISTED"> Unlisted
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Visible to anyone who knows the URL, but not shown on your profile
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Only visible to you and your collaborators"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PRIVATE"> Private
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Only visible to you and your collaborators
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,43 +3,100 @@
|
|||
<title>Configure {{tracker.owner}}/{{tracker.name}} — {{ cfg("sr.ht", "site-name") }}</title>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<form method="POST">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group {{valid.cls("tracker_name")}}">
|
||||
<label for="tracker_name">
|
||||
Name
|
||||
<span class="text-muted">(you can't edit this)</p>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tracker_name"
|
||||
id="tracker_name"
|
||||
class="form-control"
|
||||
value="{{ tracker.name }}"
|
||||
disabled />
|
||||
{{ valid.summary("tracker_name") }}
|
||||
</div>
|
||||
<div class="form-group {{ valid.cls('tracker_desc') }}">
|
||||
<label for="tracker_desc">Description</label>
|
||||
<textarea
|
||||
name="tracker_desc"
|
||||
id="tracker_desc"
|
||||
class="form-control"
|
||||
rows="5"
|
||||
aria-describedby="tracker_desc-help"
|
||||
placeholder="Markdown supported"
|
||||
>{{tracker.desc or tracker.description}}</textarea>
|
||||
{{ valid.summary("tracker_desc") }}
|
||||
</div>
|
||||
{{ valid.summary() }}
|
||||
<span class="pull-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save {{icon("caret-right")}}
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
<form class="row" method="POST">
|
||||
{{csrf_token()}}
|
||||
<div class="col-md-6">
|
||||
<div class="form-group {{valid.cls("tracker_name")}}">
|
||||
<label for="tracker_name">
|
||||
Name
|
||||
<span class="text-muted">(you can't edit this)</p>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tracker_name"
|
||||
id="tracker_name"
|
||||
class="form-control"
|
||||
value="{{ tracker.name }}"
|
||||
disabled />
|
||||
{{ valid.summary("tracker_name") }}
|
||||
</div>
|
||||
<div class="form-group {{ valid.cls('tracker_desc') }}">
|
||||
<label for="tracker_desc">Description</label>
|
||||
<textarea
|
||||
name="tracker_desc"
|
||||
id="tracker_desc"
|
||||
class="form-control"
|
||||
rows="5"
|
||||
aria-describedby="tracker_desc-help"
|
||||
placeholder="Markdown supported"
|
||||
>{{tracker.desc or tracker.description}}</textarea>
|
||||
{{ valid.summary("tracker_desc") }}
|
||||
</div>
|
||||
{{ valid.summary() }}
|
||||
<span class="pull-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save {{icon("caret-right")}}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<fieldset class="form-group">
|
||||
<legend>Tracker Visibility</legend>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PUBLIC"
|
||||
{% if tracker.visibility.value == "PUBLIC" %}
|
||||
checked
|
||||
{% endif %}
|
||||
> Public
|
||||
<small id="visibility-public-help" class="form-text text-muted">
|
||||
Shown on your profile page
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Visible to anyone with the link, but not shown on your profile"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="UNLISTED"
|
||||
{% if tracker.visibility.value == "UNLISTED" %}
|
||||
checked
|
||||
{% endif %}
|
||||
> Unlisted
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Visible to anyone who knows the URL, but not shown on your profile
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Only visible to you and your collaborators"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PRIVATE"
|
||||
{% if tracker.visibility.value == "PRIVATE" %}
|
||||
checked
|
||||
{% endif %}
|
||||
> Private
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Only visible to you and your collaborators
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -15,6 +15,22 @@
|
|||
>{{ tracker.name }}
|
||||
</h2>
|
||||
<ul class="nav nav-tabs">
|
||||
{% if tracker.visibility.value != "PUBLIC" %}
|
||||
<li
|
||||
class="nav-item nav-text vis-{{tracker.visibility.value.lower()}}"
|
||||
{% if tracker.visibility.value == "UNLISTED" %}
|
||||
title="This tracker is only visible to those who know the URL."
|
||||
{% elif tracker.visibility.value == "PRIVATE" %}
|
||||
title="This tracker is only visible to those who were invited to view it."
|
||||
{% endif %}
|
||||
>
|
||||
{% if tracker.visibility.value == "UNLISTED" %}
|
||||
Unlisted
|
||||
{% elif tracker.visibility.value == "PRIVATE" %}
|
||||
Private
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ "active" if not search else "" }}"
|
||||
href="{{ tracker | tracker_url }}">open tickets</a>
|
||||
|
@ -172,6 +188,16 @@
|
|||
An import operation is currently in progress.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if TicketAccess.browse not in access and TicketAccess.submit in access %}
|
||||
<div class="alert alert-warning">
|
||||
You do not have permission to view tickets on this tracker unless you
|
||||
submitted them.
|
||||
</div>
|
||||
{% elif TicketAccess.browse not in access and TicketAccess.submit not in access %}
|
||||
<div class="alert alert-warning">
|
||||
You do not have permission to view tickets on this tracker.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form style="margin-bottom: 0.5rem">
|
||||
<label for="search" class="sr-only">Search tickets</label>
|
||||
<input
|
||||
|
|
|
@ -21,5 +21,5 @@ from todosrht.types.ticketassignee import TicketAssignee
|
|||
from todosrht.types.ticketcomment import TicketComment
|
||||
from todosrht.types.ticketseen import TicketSeen
|
||||
from todosrht.types.ticketsubscription import TicketSubscription
|
||||
from todosrht.types.tracker import Tracker
|
||||
from todosrht.types.tracker import Tracker, Visibility
|
||||
from todosrht.types.useraccess import UserAccess
|
||||
|
|
|
@ -46,18 +46,6 @@ class Ticket(Base):
|
|||
nullable=False,
|
||||
default=TicketResolution.unresolved)
|
||||
|
||||
user_perms = sa.Column(FlagType(TicketAccess), nullable=True)
|
||||
"""Permissions given to any logged in user"""
|
||||
|
||||
submitter_perms = sa.Column(FlagType(TicketAccess), nullable=True)
|
||||
"""Permissions granted to submitters for their own tickets"""
|
||||
|
||||
committer_perms = sa.Column(FlagType(TicketAccess), nullable=True)
|
||||
"""Permissions granted to people who have authored commits in the linked git repo"""
|
||||
|
||||
anonymous_perms = sa.Column(FlagType(TicketAccess), nullable=True)
|
||||
"""Permissions granted to anonymous (non-logged in) users"""
|
||||
|
||||
view_list = sa.orm.relationship("TicketSeen", viewonly=True)
|
||||
|
||||
labels = sa.orm.relationship("Label",
|
||||
|
@ -111,14 +99,6 @@ class Ticket(Base):
|
|||
"description": self.description,
|
||||
"status": self.status.name,
|
||||
"resolution": self.resolution.name,
|
||||
"permissions": {
|
||||
"anonymous": permissions(self.anonymous_perms)
|
||||
if self.anonymous_perms else None,
|
||||
"submitter": permissions(self.submitter_perms)
|
||||
if self.submitter_perms else None,
|
||||
"user": permissions(self.user_perms)
|
||||
if self.user_perms else None,
|
||||
},
|
||||
"labels": [l.name for l in self.labels],
|
||||
"assignees": [u.to_dict(short=True) for u in self.assigned_users],
|
||||
} if not short else {}),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import re
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy_utils as sau
|
||||
import string
|
||||
from enum import Enum
|
||||
from srht.database import Base
|
||||
from srht.flagtype import FlagType
|
||||
from srht.validation import Validation
|
||||
|
@ -8,6 +10,11 @@ from todosrht.types import TicketAccess, TicketStatus, TicketResolution
|
|||
|
||||
name_re = re.compile(r"^[A-Za-z0-9._-]+$")
|
||||
|
||||
class Visibility(Enum):
|
||||
PUBLIC = 'PUBLIC'
|
||||
UNLISTED = 'UNLISTED'
|
||||
PRIVATE = 'PRIVATE'
|
||||
|
||||
class Tracker(Base):
|
||||
__tablename__ = 'tracker'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
|
@ -15,6 +22,7 @@ class Tracker(Base):
|
|||
owner = sa.orm.relationship("User", backref=sa.orm.backref("owned_trackers"))
|
||||
created = sa.Column(sa.DateTime, nullable=False)
|
||||
updated = sa.Column(sa.DateTime, nullable=False)
|
||||
visibility = sa.Column(sau.ChoiceType(Visibility), nullable=False)
|
||||
name = sa.Column(sa.Unicode(1024))
|
||||
"""
|
||||
May include slashes to serve as categories (nesting is supported,
|
||||
|
@ -35,25 +43,9 @@ class Tracker(Base):
|
|||
nullable=False,
|
||||
default=TicketResolution.fixed | TicketResolution.duplicate)
|
||||
|
||||
default_user_perms = sa.Column(FlagType(TicketAccess),
|
||||
default_access = sa.Column(FlagType(TicketAccess),
|
||||
nullable=False,
|
||||
default=TicketAccess.browse + TicketAccess.submit + TicketAccess.comment)
|
||||
"""Permissions given to any logged in user"""
|
||||
|
||||
default_submitter_perms = sa.Column(FlagType(TicketAccess),
|
||||
nullable=False,
|
||||
default=TicketAccess.browse + TicketAccess.edit + TicketAccess.comment)
|
||||
"""Permissions granted to submitters for their own tickets"""
|
||||
|
||||
default_committer_perms = sa.Column(FlagType(TicketAccess),
|
||||
nullable=False,
|
||||
default=TicketAccess.browse + TicketAccess.submit + TicketAccess.comment)
|
||||
"""Permissions granted to people who have authored commits in the linked git repo"""
|
||||
|
||||
default_anonymous_perms = sa.Column(FlagType(TicketAccess),
|
||||
nullable=False,
|
||||
default=TicketAccess.browse + TicketAccess.submit + TicketAccess.comment)
|
||||
"""Permissions granted to anonymous (non-logged in) users"""
|
||||
|
||||
import_in_progress = sa.Column(sa.Boolean,
|
||||
nullable=False, server_default='f')
|
||||
|
@ -62,6 +54,7 @@ class Tracker(Base):
|
|||
def create_from_request(request, user):
|
||||
valid = Validation(request)
|
||||
name = valid.require("name", friendly_name="Name")
|
||||
visibility = valid.require("visibility", cls=Visibility)
|
||||
desc = valid.optional("description")
|
||||
if not valid.ok:
|
||||
return None, valid
|
||||
|
@ -93,7 +86,10 @@ class Tracker(Base):
|
|||
if not valid.ok:
|
||||
return None, valid
|
||||
|
||||
tracker = Tracker(owner=user, name=name, description=desc)
|
||||
tracker = Tracker(owner=user,
|
||||
name=name,
|
||||
description=desc,
|
||||
visibility=visibility)
|
||||
|
||||
return tracker, valid
|
||||
|
||||
|
@ -119,11 +115,8 @@ class Tracker(Base):
|
|||
"name": self.name,
|
||||
**({
|
||||
"description": self.description,
|
||||
"default_permissions": {
|
||||
"anonymous": permissions(self.default_anonymous_perms),
|
||||
"submitter": permissions(self.default_submitter_perms),
|
||||
"user": permissions(self.default_user_perms),
|
||||
},
|
||||
"default_access": permissions(self.default_access),
|
||||
"visibility": self.visibility,
|
||||
} if not short else {})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue