Add initial schema

Includes:

- Trackers
- Tickets
- Custom ticket fields
- Comments
- Initial permissions
- Audit logging

Will eventually also need:

- More custom ticket field types
- API-only ticket fields
- User specific permissions
- git.sr.ht (and other gits) integrations
- Attachments (blocked pending files.sr.ht)
This commit is contained in:
Drew DeVault 2017-07-22 00:28:47 -04:00
parent ac67654f18
commit f2885f8777
9 changed files with 288 additions and 0 deletions

View File

@ -1 +1,8 @@
from .flagtype import FlagType
from .user import User
from .ticketaccess import TicketAccess
from .tracker import Tracker
from .ticketfield import TicketFieldType, TicketField
from .ticketfieldvalue import TicketFieldValue
from .ticket import Ticket
from .ticketauditentry import AuditFieldType, PermissionsTarget, TicketAuditEntry

View File

@ -0,0 +1,17 @@
import sqlalchemy.types as types
class FlagType(types.TypeDecorator):
"""
Encodes/decodes IntFlags on the fly
"""
impl = types.Integer()
def __init__(self, enum):
self.enum = enum
def process_bind_param(self, value, dialect):
return int(value)
def process_result_value(self, value, dialect):
return self.enum(value)

40
todosrht/types/ticket.py Normal file
View File

@ -0,0 +1,40 @@
import sqlalchemy as sa
from srht.database import Base
from todosrht.types import TicketAccess, FlagType
class Ticket(Base):
"""
Represents a ticket filed in the system. The default permissions are
inherited from the tracker configuration, but may be edited to i.e.
- Give an arbitrary edit/view/whatever access
- Remove a specific user's permission to edit
- Allow the public to comment on an otherwise uncommentable issue
- Lock an issue from further discussion from non-contributors
- etc
"""
__tablename__ = 'ticket'
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
ticket_id = sa.Column(sa.Integer, index=True)
"""The ID specific to this tracker, appears in URLs etc"""
name = sa.Column(sa.Unicode(2048), nullable=False)
tracker_id = sa.Column(sa.Integer, sa.ForeignKey("tracker.id"), nullable=False)
tracker = sa.orm.relationship("Tracker", backref=sa.orm.backref("tickets"))
submitter_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False)
submitter = sa.orm.relationship("User", backref=sa.orm.backref("tickets"))
default_user_perms = sa.Column(FlagType(TicketAccess), nullable=False)
"""Permissions given to any logged in user"""
default_submitter_perms = sa.Column(FlagType(TicketAccess), nullable=False)
"""Permissions granted to the ticket submitter"""
default_committer_perms = sa.Column(FlagType(TicketAccess), nullable=False)
"""Permissions granted to people who have authored commits in the linked git repo"""
default_anonymous_perms = sa.Column(FlagType(TicketAccess), nullable=False)
"""Permissions granted to anonymous (non-logged in) users"""

View File

@ -0,0 +1,9 @@
from enum import IntFlag
class TicketAccess(IntFlag):
none = 0
browse = 1
submit = 2
comment = 4
edit = 8
triage = 16

View File

@ -0,0 +1,95 @@
import sqlalchemy as sa
import sqlalchemy_utils as sau
from todosrht.types import FlagType, TicketAccess
from srht.database import Base
from enum import Enum
class AuditFieldType(Enum):
"""Describes what kind of field was updated in an audit log event"""
name = "name"
permissions = "permissions"
tracker = "tracker"
custom_field = "custom_field"
custom_event = "custom_event"
class PermissionsTarget(Enum):
"""Describes the target of an update to ticket permissions"""
anonymous = "anonymous"
logged_in = "logged_in"
submitted = "submitter"
committer = "committer"
user = "user"
"""A specific named user"""
class TicketAuditEntry(Base):
"""
Records an event that has occured to a ticket. The field_type tells you
what kind of field was affected, which is used to disambiguate the affected
columns in the database.
AuditFieldType.name is used when the ticket is renamed. old_name and
new_name are valid for these events.
AuditFieldType.permissions is used permissions are changed. old_permissions
and new_permissions are valid for these events, as well as
permissions_target, which describes what kind of user was affected by the
change. If permissions_target == PermissionsTarget.user, a specific user's
permissions were edited and permissions_user is valid.
AuditFieldType.tracker is used when a ticket is moved between trackers.
old_tracker and new_tracker are valid for this event.
AuditFieldType.custom_field is when a custom field is edited.
old_custom_value and new_custom_value are valid for this event.
AuditFieldType.custom_event is used for events submitted through the API
(i.e. build status updates). oauth_client and custom_text are valid for
this event.
"""
__tablename__ = 'ticket_audit_entry'
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False)
ticket = sa.orm.relationship("Ticket",
backref=sa.orm.backref("audit_log"))
field_type = sa.Column(sau.ChoiceType(AuditFieldType), nullable=False)
user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False)
user = sa.orm.relationship("User",
foreign_keys=[user_id])
"""The user who executed the change"""
ticket_field_id = sa.Column(sa.Integer, sa.ForeignKey("ticket_field.id"))
ticket_field = sa.orm.relationship("TicketField")
#oauth_client_id = sa.Column(sa.Integer, sa.ForeignKey("oauth_client.id"))
#oauth_client = sa.orm.relationship("OAuthClient")
custom_text = sa.Column(sa.Unicode(4096))
"""Markdown, typically used for custom events submitted via API"""
old_name = sa.Column(sa.Unicode(2048))
new_name = sa.Column(sa.Unicode(2048))
old_permissions = sa.Column(FlagType(TicketAccess))
new_permissions = sa.Column(FlagType(TicketAccess))
permissions_target = sa.Column(sau.ChoiceType(PermissionsTarget))
permissions_user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"))
permissions_user = sa.orm.relationship("User",
foreign_keys=[permissions_user_id])
old_tracker_id = sa.Column(sa.Integer,
sa.ForeignKey("tracker.id"))
old_tracker = sa.orm.relationship("Tracker",
foreign_keys=[old_tracker_id])
new_tracker_id = sa.Column(sa.Integer,
sa.ForeignKey("tracker.id"))
new_tracker = sa.orm.relationship("Tracker",
foreign_keys=[new_tracker_id])
old_custom_value_id = sa.Column(sa.Integer,
sa.ForeignKey("ticket_field_value.id"))
old_custom_value = sa.orm.relationship("TicketFieldValue",
foreign_keys=[old_custom_value_id])
new_custom_value_id = sa.Column(sa.Integer,
sa.ForeignKey("ticket_field_value.id"))
new_custom_value = sa.orm.relationship("TicketFieldValue",
foreign_keys=[new_custom_value_id])

View File

@ -0,0 +1,19 @@
import sqlalchemy as sa
from srht.database import Base
class TicketComment(Base):
__tablename__ = "ticket_comment"
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False)
ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("fields"))
user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False)
user = sa.orm.relationship("User", backref=sa.orm.backref("comments"))
text = sa.Column(sa.Unicode(16384), nullable=False)
"""Markdown"""
visible = sa.Column(sa.Boolean, nullable=False, default=True)
"""Deleted comments stay in the system, but are removed from the listing"""

View File

@ -0,0 +1,39 @@
import sqlalchemy as sa
import sqlalchemy_utils as sau
from srht.database import Base
from todosrht.types import FlagType, TicketAccess
from enum import Enum
class TicketFieldType(Enum):
string = "string"
"""A single line of text"""
multiline = "multiline"
"""Multiple lines of text"""
markdown = "markdown"
"""Markdown text"""
# TODO: option, multioption, boolean
user_agent = "user_agent"
"""Value of User-Agent header when ticket is submitted"""
# TODO: decoded user agent fields
# TODO: user, user_list, git_tag, git_repo
class TicketField(Base):
"""
Represents a field given to the user to fill out. All tickets have a few
fields like name and submitter that cannot be changed.
"""
__tablename__ = 'ticket_field'
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
tracker_id = sa.Column(sa.Integer, sa.ForeignKey("tracker.id"), nullable=False)
tracker = sa.orm.relationship("Tracker", backref=sa.orm.backref("fields"))
label = sa.Column(sa.Unicode(1024))
field_type = sa.Column(sau.ChoiceType(TicketFieldType), nullable=False)
order = sa.Column(sa.Integer, nullable=False, default=0)
requried = sa.Column(sa.Boolean, nullable=False, default=False)
default_value = sa.Column(sa.Unicode(16384))
default_perms = sa.Column(FlagType(TicketAccess),
nullable=False,
default=TicketAccess.edit)
"""Users with this level of access to the ticket can edit this field"""

View File

@ -0,0 +1,20 @@
import sqlalchemy as sa
from srht.database import Base
class TicketFieldValue(Base):
"""Associates a ticket field with its values for a given ticket"""
__tablename__ = 'ticket_field_value'
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"))
"""
Note: this can be None if the ticket was edited; we keep the row around for
the audit log and keep the association via the audit log table
"""
ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("fields"))
ticket_field_id = sa.Column(sa.Integer,
sa.ForeignKey("ticket_field.id"),
nullable=False)
ticket_field = sa.orm.relationship("TicketField")
string_value = sa.Column(sa.Unicode(16384))

42
todosrht/types/tracker.py Normal file
View File

@ -0,0 +1,42 @@
import sqlalchemy as sa
from srht.database import Base
from todosrht.types import TicketAccess, FlagType
class Tracker(Base):
__tablename__ = 'tracker'
id = sa.Column(sa.Integer, primary_key=True)
owner_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False)
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)
name = sa.Column(sa.Unicode(1024))
"""
May include slashes to serve as categories (nesting is supported,
builds.sr.ht style)
"""
description = sa.Column(sa.Unicode(8192))
"""Markdown"""
default_user_perms = 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)
"""Permissions granted to anonymous (non-logged in) users"""
def __repr__(self):
return '<Tracker {} {}>'.format(self.id, self.name)