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:
Drew DeVault 2021-09-22 12:24:35 +02:00
parent 4521acc3ca
commit d2f57b98c1
13 changed files with 381 additions and 233 deletions

View File

@ -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

View File

@ -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;
""")

View File

@ -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;
""")

View File

@ -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:

View File

@ -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(),

View File

@ -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")

View File

@ -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>

View File

@ -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 &amp; 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 &amp; 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 %}

View File

@ -3,43 +3,100 @@
<title>Configure {{tracker.owner}}/{{tracker.name}} &mdash; {{ 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 %}

View File

@ -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

View File

@ -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

View File

@ -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 {}),

View File

@ -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 {})
}