import random
from onegov.core.collection import Pagination
from onegov.ticket import handlers as global_handlers
from onegov.ticket.model import Ticket
from sqlalchemy import desc, distinct, func
from sqlalchemy.orm import joinedload, undefer
from uuid import UUID
from typing import Any, Literal, NamedTuple, Self, TYPE_CHECKING
if TYPE_CHECKING:
from onegov.ticket.model import TicketState
from sqlalchemy.orm import Query, Session
from typing import TypeAlias, TypedDict
[docs]
ExtendedTicketState: TypeAlias = TicketState | Literal['all', 'unfinished']
class StateCountDict(TypedDict, total=False):
open: int
pending: int
closed: int
archived: int
[docs]
class TicketCount(NamedTuple):
[docs]
class TicketCollection(TicketCollectionPagination):
[docs]
def query(self) -> 'Query[Ticket]':
return self.session.query(Ticket)
[docs]
def random_number(self, length: int) -> int:
range_start = 10 ** (length - 1)
range_end = 10 ** length - 1
return random.randint(range_start, range_end) # nosec B311
[docs]
def random_ticket_number(self, handler_code: str) -> str:
number = str(self.random_number(length=8))
return f'{handler_code}-{number[:4]}-{number[4:]}'
[docs]
def is_existing_ticket_number(self, ticket_number: str) -> bool:
query = self.query().filter(Ticket.number == ticket_number)
return self.session.query(query.exists()).scalar()
[docs]
def issue_unique_ticket_number(self, handler_code: str) -> str:
""" Randomly generates a new ticket number, ensuring it is unique
for the given handler_code.
The resulting code is of the following form::
XXX-0000-1111
Where ``XXX`` is the handler_code and the rest is a 12 character
sequence of random numbers separated by dashes.
This gives us 10^8 or 100 million ticket numbers for each handler.
Though we'll never reach that limit, there is an increasing chance
of conflict with existing ticket numbers, so we have to check
against the database.
Still, this number is not unguessable (say in an URL) - there we have
to rely on the internal ticket id, which is a uuid.
In a social engineering setting, where we don't have the abilty to
quickly try out thousands of numbers, the ticket number should
be pretty unguessable however.
"""
# usually we won't have any conflict, so we just run queries
# against the existing database, even if this means to run more than
# one query once in forever
while True:
candidate = self.random_ticket_number(handler_code)
if not self.is_existing_ticket_number(candidate):
return candidate
[docs]
def open_ticket(
self,
handler_code: str,
handler_id: str,
**handler_data: Any
) -> Ticket:
""" Opens a new ticket using the given handler. """
ticket = Ticket.get_polymorphic_class(handler_code, default=Ticket)()
ticket.number = self.issue_unique_ticket_number(handler_code)
# add it to the session before invoking the handler, who expects
# each ticket to belong to a session already
self.session.add(ticket)
ticket.handler_id = handler_id
ticket.handler_code = handler_code
ticket.handler_data = handler_data
ticket.handler.refresh()
self.session.flush()
return ticket
# FIXME: It seems better to return a query here...
[docs]
def by_handler_code(self, handler_code: str) -> list[Ticket]:
return self.query().filter(Ticket.handler_code == handler_code).all()
[docs]
def by_id(
self,
id: UUID,
ensure_handler_code: str | None = None
) -> Ticket | None:
query = self.query().filter(Ticket.id == id)
if ensure_handler_code:
query = query.filter(Ticket.handler_code == ensure_handler_code)
return query.first()
[docs]
def by_handler_id(self, handler_id: str) -> Ticket | None:
return self.query().filter(Ticket.handler_id == handler_id).first()
[docs]
def get_count(self, excl_archived: bool = True) -> TicketCount:
query: Query[tuple[str, int]] = self.query().with_entities(
Ticket.state, func.count(Ticket.state)
)
if excl_archived:
query = query.filter(Ticket.state != 'archived')
query = query.group_by(Ticket.state)
return TicketCount(**dict(query))
[docs]
def by_handler_data_id(
self,
handler_data_id: str | UUID
) -> 'Query[Ticket]':
return self.query().filter(
Ticket.handler_data['handler_data']['id'] == str(handler_data_id))
# FIXME: Why is this its own subclass? shouldn't this at least override
# __init__ to pin state to 'archived'?!
[docs]
class ArchivedTicketCollection(TicketCollectionPagination):
[docs]
def query(self) -> 'Query[Ticket]':
return self.session.query(Ticket)