Esempio n. 1
0
def test_utcnow():

    assert sedate.utcnow().replace(
        tzinfo=None, microsecond=0, second=0, minute=0) \
        == datetime.utcnow().replace(microsecond=0, second=0, minute=0)

    assert sedate.utcnow().tzinfo == UTC
Esempio n. 2
0
def test_notices_pdf_from_notices(gazette_app):
    session = gazette_app.session()

    with freeze_time("2017-01-01 12:00"):
        notice = GazetteNotice(title='first title',
                               text='first text',
                               author_place='first place',
                               author_date=utcnow(),
                               author_name='first author',
                               state='submitted')
        notice.files.append(pdf_attachment('first attachment'))
        session.add(notice)
        session.flush()

    with freeze_time("2017-01-02 12:00"):
        notice = GazetteNotice(title='second title',
                               text='second text',
                               author_place='second place',
                               author_date=utcnow(),
                               author_name='second author',
                               state='submitted')
        session.add(notice)
        session.flush()

    with freeze_time("2018-01-01 12:00"):
        request = DummyRequest(session, gazette_app.principal)
        notices = GazetteNoticeCollection(session)
        file = NoticesPdf.from_notices(notices, request)
        reader = PdfFileReader(file)
        assert [page.extractText() for page in reader.pages
                ] == [('© 2018 Govikon\n1\n'
                       'xxx\nfirst title\nfirst text\n'
                       'first place, 1. Januar 2017\nfirst author\n'),
                      '© 2018 Govikon\n2\n',
                      ('© 2018 Govikon\n3\n'
                       'xxx\nsecond title\nsecond text\n'
                       'second place, 2. Januar 2017\nsecond author\n')]

        file = NoticesPdf.from_notices(notices.for_order('title', 'desc'),
                                       request)
        reader = PdfFileReader(file)
        assert [page.extractText() for page in reader.pages
                ] == [('© 2018 Govikon\n1\n'
                       'xxx\nsecond title\nsecond text\n'
                       'second place, 2. Januar 2017\nsecond author\n'
                       'xxx\nfirst title\nfirst text\n'
                       'first place, 1. Januar 2017\nfirst author\n'),
                      '© 2018 Govikon\n2\n']
Esempio n. 3
0
def test_issues_pdf_notice(gazette_app):
    session = gazette_app.session()

    with freeze_time("2017-01-01 12:00"):
        notice = GazetteNotice(title='title',
                               text='text',
                               author_place='place',
                               author_date=utcnow(),
                               author_name='author',
                               state='drafted')
        notice.files.append(pdf_attachment('attachment'))
        session.add(notice)
        session.flush()

    layout = Layout(notice, DummyRequest(session, gazette_app.principal))

    pdf = IssuePdf(BytesIO())
    pdf.init_a4_portrait()

    pdf.notice(notice, layout, '1')
    assert extract_pdf_story(pdf) == [
        '1', 'title', 'text', 'place, 1. Januar 2017<br/>author'
    ]

    notice.print_only = True
    pdf.notice(notice, layout, '2')
    assert extract_pdf_story(pdf) == [
        '1', 'title', 'text', 'place, 1. Januar 2017<br/>author', '2',
        '<i>This official notice is only available in the print version.</i>'
    ]
Esempio n. 4
0
def has_permission_scan_job(app, identity, model, permission):
    # Editors and members of groups may view and edit scan jobs within
    # the same group
    if identity.role in ('editor', 'member'):
        if identity.groupid:

            if permission in {ViewModel, EditModel}:
                if same_group(model, identity):
                    return True

            if permission is DeleteModel and identity.role == 'editor':
                if same_group(model, identity):
                    dt = model.dispatch_date

                    # editors of the same group may delete scan jobs up until
                    # 17:00 on the day before the dispatch
                    horizon = datetime(dt.year, dt.month, dt.day, 17)
                    horizon -= timedelta(days=1)
                    horizon = sedate.replace_timezone(horizon, 'Europe/Zurich')

                    now = sedate.utcnow()

                    return now <= horizon

    return permission in getattr(app.settings.roles, identity.role)
Esempio n. 5
0
    def remove_reservation_from_session(self, session_id, token):
        """ Removes the reservation with the given session_id and token. """

        assert token and session_id

        query = self.reservations_by_session(session_id)
        query = query.filter(Reservation.token == token)

        reservation = query.one()
        self.session.delete(reservation)

        # if we get here the token must be valid, we should then check if the
        # token is used in the reserved slots, because with autoapproval these
        # slots may be created straight away.

        slots = self.session.query(ReservedSlot).filter(
            ReservedSlot.reservation_token == token
        )

        slots.delete('fetch')

        # we also update the timestamp of existing reservations within
        # the same session to ensure that we account for the user's activity
        # properly during the session expiration cronjob. Otherwise it is
        # possible that a user removes the latest reservations only to see
        # the rest of them vanish because his older reservations were
        # already old enough to be counted as expired.

        query = self.session.query(Reservation)
        query = query.filter(Reservation.session_id == session_id)

        query.update({"modified": sedate.utcnow()})
Esempio n. 6
0
def test_signature_timestamp(session):
    path = module_path('onegov.file', 'tests/fixtures/sample.pdf')
    time = sedate.utcnow()

    with open(path, 'rb') as f:
        session.add(File(name='sample.pdf', reference=f, signature_metadata={
            'timestamp': time.isoformat()
        }))

    transaction.commit()

    # if unsinged, the timestamp is ignored
    assert session.query(File).one().signature_timestamp is None
    assert session.query(File).with_entities(File.signature_timestamp).one()\
        .signature_timestamp is None

    # if signed the timestamp is in UTC (not timezone-aware)
    session.query(File).one().signed = True
    transaction.commit()

    assert session.query(File).one().signature_timestamp == time
    assert session.query(File).with_entities(File.signature_timestamp).one()\
        .signature_timestamp == time

    # make sure we can filter by time
    assert session.query(File).filter_by(signature_timestamp=time).first()

    # make sure we get utc for both
    assert session.query(File).one()\
        .signature_timestamp.tzinfo.zone == 'UTC'
    assert session.query(File).with_entities(File.signature_timestamp).one()\
        .signature_timestamp.tzinfo.zone == 'UTC'
Esempio n. 7
0
    def overdue_issues(self):
        """ Returns True, if any of the issue's deadline is reached. """

        if self._issues:
            query = object_session(self).query(Issue)
            query = query.filter(Issue.name.in_(self._issues.keys()))
            query = query.filter(Issue.deadline < utcnow())
            if query.first():
                return True

        return False
Esempio n. 8
0
def handle_send_notification(self, request, form):

    period = PeriodCollection(request.session).active()
    variables = TemplateVariables(request, period)
    layout = NotificationTemplateLayout(self, request)

    if form.submitted(request):
        recipients = form.recipients

        if not recipients:
            request.alert(_("There are no recipients matching the selection"))
        else:
            current = request.current_username

            if current not in recipients:
                recipients.add(current)

            subject = variables.render(self.subject)
            content = render_template('mail_notification.pt', request, {
                'layout': DefaultMailLayout(self, request),
                'title': subject,
                'notification': variables.render(self.text)
            })
            plaintext = html_to_text(content)

            for recipient in recipients:
                request.app.send_marketing_email(
                    receivers=(recipient, ),
                    subject=subject,
                    content=content,
                    plaintext=plaintext,
                )

            self.last_sent = utcnow()

            request.success(_(
                "Successfully sent the e-mail to ${count} recipients",
                mapping={
                    'count': len(recipients)
                }
            ))

            return request.redirect(
                request.class_link(NotificationTemplateCollection))

    return {
        'title': _("Mailing"),
        'layout': layout,
        'form': form,
        'preview_subject': variables.render(self.subject),
        'preview_body': variables.render(self.text),
        'edit_link': request.return_here(request.link(self, 'edit')),
        'button_text': _("Send E-Mail Now")
    }
Esempio n. 9
0
def view_dashboard(self, request):
    """ The dashboard view (for editors).

    Shows the drafted, submitted and rejected notices, shows warnings and
    allows to create a new notice.

    """
    layout = Layout(self, request)

    user_ids, group_ids = get_user_and_group(request)
    collection = GazetteNoticeCollection(request.session,
                                         user_ids=user_ids,
                                         group_ids=group_ids)

    # rejected
    rejected = collection.for_state('rejected').query().all()
    if rejected:
        request.message(_("You have rejected messages."), 'warning')

    # drafted
    drafted = collection.for_state('drafted').query().all()
    now = utcnow()
    limit = now + timedelta(days=2)
    past_issues_selected = False
    deadline_reached_soon = False
    for notice in drafted:
        for issue in notice.issue_objects:
            if issue.deadline < now:
                past_issues_selected = True
            elif issue.deadline < limit:
                deadline_reached_soon = True
    if past_issues_selected:
        request.message(_("You have drafted messages with past issues."),
                        'warning')
    if deadline_reached_soon:
        request.message(
            _("You have drafted messages with issues close to the deadline."),
            'warning')

    # submitted
    submitted = collection.for_state('submitted').query().all()

    new_notice = request.link(collection.for_state('drafted'),
                              name='new-notice')

    return {
        'layout': layout,
        'title': _("Dashboard"),
        'rejected': rejected,
        'drafted': drafted,
        'submitted': submitted,
        'new_notice': new_notice,
        'current_issue': layout.current_issue
    }
Esempio n. 10
0
def test_sign_file(app):
    tape = module_path('onegov.file', 'tests/cassettes/ais-success.json')

    with vcr.use_cassette(tape, record_mode='none'):
        ensure_correct_depot(app)

        transaction.begin()

        path = module_path('onegov.file', 'tests/fixtures/sample.pdf')

        with open(path, 'rb') as f:
            app.session().add(File(name='sample.pdf', reference=f))

        with open(path, 'rb') as f:
            old_digest = hashlib.sha256(f.read()).hexdigest()

        transaction.commit()
        pdf = app.session().query(File).one()

        token = 'ccccccbcgujhingjrdejhgfnuetrgigvejhhgbkugded'

        with patch.object(Yubico, 'verify') as verify:
            verify.return_value = True

            app.sign_file(file=pdf, signee='*****@*****.**', token=token)

            transaction.commit()
            pdf = app.session().query(File).one()

            assert pdf.signed
            assert pdf.reference['content_type'] == 'application/pdf'
            assert pdf.signature_metadata['signee'] == '*****@*****.**'
            assert pdf.signature_metadata['old_digest'] == old_digest
            assert pdf.signature_metadata['new_digest']
            assert pdf.signature_metadata['token'] == token
            assert pdf.signature_metadata['token_type'] == 'yubikey'
            assert pdf.signature_metadata['request_id']\
                .startswith('swisscom_ais/foo/')

            assert len(pdf.reference.file.read()) > 0

            timestamp = isodate.parse_datetime(
                pdf.signature_metadata['timestamp'])

            now = sedate.utcnow()
            assert (now - timedelta(seconds=10)) <= timestamp <= now

            with pytest.raises(RuntimeError) as e:
                app.sign_file(pdf, signee='*****@*****.**', token=token)

            assert "already been signed" in str(e)
Esempio n. 11
0
def view_latest_event(self, request):
    """ Redirects to the latest occurrence of an event that is, either the
    next future event or the last event in the past if there are no more
    future events.

    """

    now = utcnow()

    for occurrence in self.occurrences:
        if now < occurrence.start:
            return morepath.redirect(request.link(occurrence))

    return morepath.redirect(request.link(occurrence))
Esempio n. 12
0
    def sections(self):
        now = sedate.utcnow()

        sections = (self.__class__({
            'Id': r['TeilbaustelleId'],
            'Teilbaustellen': [],
            **r
        }) for r in self['Teilbaustellen'])

        sections = (s for s in sections if s['DauerVon'])
        sections = (s for s in sections if s['DauerVon'] <= now)
        sections = (s for s in sections if now <= (s['DauerBis'] or now))

        return list(sections)
Esempio n. 13
0
    def publish_files(self, horizon=None):
        """ Publishes unpublished files with a publish date older than the
        given horizon.

        """
        # default to a horizon slightly into the future as this method is
        # usually called by cronjob which is not perfectly on time
        horizon = horizon or (utcnow() + timedelta(seconds=90))

        for f in self.publishable_files(horizon):
            f.published = True
            f.publish_date = None

        self.session.flush()
Esempio n. 14
0
    def on_request(self):
        session = self.request.session

        # populate organization (active root elements with no children or
        # active children (but not their parents))
        self.organization.choices = []
        self.organization.choices.append(
            ('', self.request.translate(_("Select one"))))
        query = session.query(Organization)
        query = query.filter(Organization.active.is_(True))
        query = query.filter(Organization.parent_id.is_(None))
        query = query.order_by(Organization.order)
        for root in query:
            if root.children:
                for child in root.children:
                    if child.active:
                        self.organization.choices.append(
                            (child.name, child.title))
            else:
                self.organization.choices.append((root.name, root.title))

        # populate categories
        query = session.query(Category.name, Category.title)
        query = query.filter(Category.active.is_(True))
        query = query.order_by(Category.order)
        self.category.choices = query.all()

        # populate issues
        now = utcnow()
        layout = Layout(None, self.request)

        self.issues.choices = []
        query = session.query(Issue)
        query = query.order_by(Issue.date)
        if self.request.is_private(self.model):
            query = query.filter(date.today() < Issue.date)  # publisher
        else:
            query = query.filter(now < Issue.deadline)  # editor
        for issue in query:
            self.issues.choices.append(
                (issue.name,
                 layout.format_issue(issue, date_format='date_with_weekday')))
            if now >= issue.deadline:
                self.issues.render_kw['data-hot-issue'] = issue.name

        # Remove the print only option if not publisher
        if not self.request.is_private(self.model):
            self.delete_field('print_only')
Esempio n. 15
0
    def events(self, request):
        session = request.session
        stmt = self.attendee_calendar

        records = session.execute(
            select(stmt.c).where(
                and_(stmt.c.attendee_id == self.attendee_id,
                     stmt.c.state == 'accepted', stmt.c.confirmed == True)))

        datestamp = utcnow()

        for record in records:
            event = icalendar.Event()

            event.add('uid', record.uid)
            event.add('summary', record.title)

            if record.note:
                event.add('description', record.note)

            event.add('dtstart', standardize_date(record.start, 'UTC'))
            event.add('dtend', standardize_date(record.end, 'UTC'))
            event.add('dtstamp', datestamp)
            event.add(
                'url',
                request.class_link(VacationActivity, {'name': record.name}))

            if record.meeting_point:
                event.add('location', record.meeting_point)

            if record.lat and record.lon:
                event.add('geo', (float(record.lat), float(record.lon)))

            if record.meeting_point and record.lat and record.lon:
                event.add("X-APPLE-STRUCTURED-LOCATION",
                          f"geo:{record.lat},{record.lon}",
                          parameters={
                              "VALUE": "URI",
                              "X-ADDRESS": record.meeting_point,
                              "X-APPLE-RADIUS": "50",
                              "X-TITLE": record.meeting_point
                          })

            yield event
Esempio n. 16
0
    def find_expired_reservation_sessions(self, expiration_date):
        """ Goes through all reservations and returns the session ids of the
        unconfirmed ones which are older than the given expiration date.
        By default the expiration date is now - 15 minutes.

        Note that this method goes through ALL RESERVATIONS OF THE CURRENT
        SESSION. This is NOT limited to a specific context or scheduler.

        """

        expiration_date = expiration_date or (
            sedate.utcnow() - timedelta(minutes=15)
        )

        # first get the session ids which are expired
        query = self.session.query(
            Reservation.session_id,
            func.max(Reservation.created),
            func.max(Reservation.modified)
        )

        query = query.group_by(Reservation.session_id)

        # != null() because != None is not allowed by PEP8
        query = query.filter(Reservation.session_id != null())

        # only pending reservations are considered
        query = query.filter(Reservation.status == 'pending')

        # the idea is to remove all reservations belonging to sessions whose
        # latest update is expired - either delete the whole session or let
        # all of it be
        expired_sessions = []

        for session_id, created, modified in query.all():

            modified = modified or created
            assert created and modified

            if max(created, modified) < expiration_date:
                expired_sessions.append(session_id)

        return expired_sessions
Esempio n. 17
0
def propose_activity(self, request):
    assert request.app.active_period, "An active period is required"

    # if the latest request has been done in the last minute, this is a
    # duplicate and should be ignored
    latest = self.latest_request

    if latest and (sedate.utcnow() - timedelta(seconds=60)) < latest.created:
        return

    session = request.session

    with session.no_autoflush:
        self.propose()

        publication_request = self.create_publication_request(
            request.app.active_period)

        ticket = TicketCollection(session).open_ticket(
            handler_code='FER', handler_id=publication_request.id.hex
        )
        TicketMessage.create(ticket, request, 'opened')

    send_ticket_mail(
        request=request,
        template='mail_ticket_opened.pt',
        subject=_("Your ticket has been opened"),
        receivers=(self.username, ),
        ticket=ticket,
        force=(
            request.is_organiser_only
            or request.current_username != self.username
        )
    )

    request.success(_("Thank you for your proposal!"))

    @request.after
    def redirect_intercooler(response):
        response.headers.add('X-IC-Redirect', request.link(ticket, 'status'))

    # do not redirect here, intercooler doesn't deal well with that...
    return
Esempio n. 18
0
def test_notices_pdf_from_notice(gazette_app):
    session = gazette_app.session()

    with freeze_time("2017-01-01 12:00"):
        notice = GazetteNotice(title='title',
                               text='text',
                               author_place='place',
                               author_date=utcnow(),
                               author_name='author',
                               state='drafted')
        notice.files.append(pdf_attachment('attachment'))
        session.add(notice)
        session.flush()

    with freeze_time("2018-01-01 12:00"):
        request = DummyRequest(session, gazette_app.principal)
        file = NoticesPdf.from_notice(notice, request)
        reader = PdfFileReader(file)
        assert [page.extractText() for page in reader.pages] == [
            '© 2018 Govikon\n1\n'
            'xxx\ntitle\ntext\nplace, 1. Januar 2017\nauthor\n',
            '© 2018 Govikon\n2\n'
        ]
Esempio n. 19
0
 def timestamp():
     return sedate.utcnow()
Esempio n. 20
0
 def current_issue(self):
     return self.query().filter(Issue.deadline > utcnow()).first()
Esempio n. 21
0
    def expired(self):
        """ Returns True, if the notice is expired. """

        if self.expiry_date:
            return self.expiry_date < utcnow()
        return False
Esempio n. 22
0
    def sign_file(self, file, signee, token, token_type='yubikey'):
        """ Signs the given file and stores metadata about that process.

        During signing the stored file is replaced with the signed version.

        For example::

            pdf = app.sign_file(pdf, '*****@*****.**', 'foo')

        :param file:

            The :class:`onegov.file..File` instance to sign.

        :param signee:

            The name of the signee (should be a username).

        :param token:

            The (yubikey) token used to sign the file.

            WARNING: It is the job of the caller to ensure that the yubikey has
            the right to sign the document (i.e. that it is the right yubikey).

        :param token_type:

            They type of the passed token. Currently only 'yubikey'.

        """

        if file.signed:
            raise AlreadySignedError(file)

        if token_type == 'yubikey':
            def is_valid_token(token):
                if not getattr(self, 'yubikey_client_id', None):
                    raise TokenConfigurationError(token_type)

                return is_valid_yubikey(
                    self.yubikey_client_id,
                    self.yubikey_secret_key,
                    expected_yubikey_id=yubikey_public_id(token),
                    yubikey=token
                )
        else:
            raise NotImplementedError(f"Unknown token type: {token_type}")

        if not is_valid_token(token):
            raise InvalidTokenError(token)

        mb = 1024 ** 2
        session = object_session(file)

        with SpooledTemporaryFile(max_size=16 * mb, mode='wb') as signed:
            old_digest = digest(file.reference.file)
            request_id = self.signing_service.sign(file.reference.file, signed)
            new_digest = digest(signed)

            signed.seek(0)

            file.reference = FileIntent(
                fileobj=signed,
                filename=file.name,
                content_type=file.reference['content_type'])

        file.signature_metadata = {
            'old_digest': old_digest,
            'new_digest': new_digest,
            'signee': signee,
            'timestamp': utcnow().isoformat(),
            'request_id': request_id,
            'token': token,
            'token_type': token_type
        }

        file.signed = True

        from onegov.file.models.file_message import FileMessage  # circular
        FileMessage.log_signature(file, signee)

        session.flush()
Esempio n. 23
0
def test_process_time(session):

    user = User()

    with freeze_time('2016-06-21') as frozen:

        # the created timestamp would usually be set as the session is flushed
        ticket = Ticket(state='open', created=Ticket.timestamp())

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is 0
        assert ticket.last_state_change is None

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is 10
        assert ticket.last_state_change is None

        ticket.accept_ticket(user)

        assert ticket.reaction_time == 10
        assert ticket.process_time is None
        assert ticket.current_process_time == 0
        assert ticket.last_state_change == utcnow()

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time == 10
        assert ticket.process_time is None
        assert ticket.current_process_time == 10
        assert ticket.last_state_change == utcnow() - timedelta(seconds=10)

        ticket.close_ticket()

        assert ticket.reaction_time == 10
        assert ticket.process_time == 10
        assert ticket.current_process_time == 10
        assert ticket.last_state_change == utcnow()

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time == 10
        assert ticket.process_time == 10
        assert ticket.current_process_time == 10
        assert ticket.last_state_change == utcnow() - timedelta(seconds=10)

        ticket.reopen_ticket(user)

        assert ticket.reaction_time == 10
        assert ticket.process_time == 10
        assert ticket.current_process_time == 10
        assert ticket.last_state_change == utcnow()

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time == 10
        assert ticket.process_time == 10
        assert ticket.current_process_time == 20
        assert ticket.last_state_change == utcnow() - timedelta(seconds=10)

        ticket.close_ticket()

        assert ticket.reaction_time == 10
        assert ticket.process_time == 20
        assert ticket.current_process_time == 20
        assert ticket.last_state_change == utcnow()
Esempio n. 24
0
def test_legacy_process_time(session):
    """ Tests the process_time/response_time for existing tickets, which cannot
    be migrated as this information cannot be inferred.

    """

    user = User()

    # test if the changes work for existing pending tickets (we don't need
    # to check the open tickets, as there is no difference between an open
    # ticket before/after the lead time introduction)
    with freeze_time('2016-06-21') as frozen:
        ticket = Ticket(state='pending', created=Ticket.timestamp(), user=user)

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change is None

        ticket.close_ticket()

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow()

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow() - timedelta(seconds=10)

        ticket.reopen_ticket(user)

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow()

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow() - timedelta(seconds=10)

        ticket.close_ticket()

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow()

    # test if the changes work for existing closed tickets
    with freeze_time('2016-06-21') as frozen:
        ticket = Ticket(state='closed', created=Ticket.timestamp())

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change is None

        ticket.reopen_ticket(user)

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow()

        frozen.tick(delta=timedelta(seconds=10))

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow() - timedelta(seconds=10)

        ticket.close_ticket()

        assert ticket.reaction_time is None
        assert ticket.process_time is None
        assert ticket.current_process_time is None
        assert ticket.last_state_change == utcnow()