Esempio n. 1
0
    def test_web_promote(self):
        db = self.app.database
        from .. import emailpw
        with db.transaction() as conn:
            user = emailpw.User('*****@*****.**', 'Tester', 'tester')
            user.setpw(b'secret')
            uid = user.id
            epw = conn.root.sites['localhost'].auth
            epw.users_by_uid[user.id] = user
            epw.users_by_email[user.email] = user
            user = emailpw.User('*****@*****.**', 'Super', 'super', True)
            user.setpw(b'supersecret')
            epw.users_by_uid[user.id] = user
            epw.users_by_email[user.email] = user

        app = self._test_app()
        self._login(app, '*****@*****.**', 'supersecret')

        # Now, we'll updata the user's admin flag
        r = app.put_json('/auth/users/' + uid + '/type', dict(admin=True))
        vars = Vars()
        self.assertEqual(
            {
                'updates': {
                    'generation': vars.generation,
                    'site': {
                        'boards': [],
                        'users': vars.users
                    },
                    'zoid': vars.zoid,
                    'user': vars.user
                }
            }, r.json)
        self.assertEqual(True,
                         [u for u in vars.users if u['id'] == uid][0]['admin'])

        # Now demote
        r = app.put_json('/auth/users/' + uid + '/type', dict(admin=False))
        vars = Vars()
        self.assertEqual(
            {
                'updates': {
                    'generation': vars.generation,
                    'site': {
                        'boards': [],
                        'users': vars.users
                    },
                    'zoid': vars.zoid,
                    'user': vars.user
                }
            }, r.json)
        self.assertEqual(False,
                         [u for u in vars.users if u['id'] == uid][0]['admin'])
Esempio n. 2
0
    def test_archive_and_restore(self):
        vars = Vars()
        with self._app.database.transaction() as conn:
            site = get_site(conn.root, 'localhost')
            site.add_board('test', '', '')
            board = site.boards['test']
            board.new_project('p1', 0)
            [p1] = board.tasks
            board.new_task(p1.id, 't1', 1)
            board.new_task(p1.id, 't2', 2)
            task_ids = sorted(t.id for t in board.tasks)
            board.new_project('p2', 3)

        self.get('/board/test/poll') # set generation

        r = self.app.post('/board/test/archive/' + p1.id)
        updates = r.json['updates']
        self.assertEqual(dict(archive_count=1,
                              description='', name='test', title=''),
                         updates['board'])
        self.assertEqual(dict(removals=vars.removals), updates['tasks'])
        self.assertEqual(sorted(vars.removals), task_ids)

        r = self.app.delete('/board/test/archive/' + p1.id)
        updates = r.json['updates']
        self.assertEqual(dict(archive_count=0,
                              description='', name='test', title=''),
                         updates['board'])
        self.assertEqual(dict(adds=vars.restores), updates['tasks'])
        self.assertEqual(sorted(t['id'] for t in vars.restores), task_ids)
Esempio n. 3
0
    def setUp(self):
        self._app = make_app()
        with self._app.database.transaction() as conn:
            get_site(conn.root, 'localhost', 'Test site').auth = auth.Admin()

        self.app = self._test_app()
        self.vars = Vars()
Esempio n. 4
0
    def test_site_updates(self):
        self.site.add_board('first', 'The first one', 'Yup, the first')
        self.site.update_users(users)

        self.assertEqual(
            {'generation': Vars().x,
             'site': self.site, 'zoid': str(u64(self.site.changes._p_oid))},
            self.site.updates(0))
Esempio n. 5
0
    def test_new_project(self):
        self.board.new_project('first', 42, 'Do First thing')
        vars = Vars()
        self.assertEqual(
            dict(generation=vars.generation, tasks=dict(adds=[vars.project])),
            self.board.updates(self.board_generation))
        self.assertEqual('first', vars.project.title)
        self.assertEqual(42, vars.project.order)
        self.assertEqual('Do First thing', vars.project.description)

        self.assertTrue(vars.generation > self.board_generation)
Esempio n. 6
0
    def test_update(self):
        self.board.update('do', 'Do things', 'Get things done')
        self.assertEqual(
            dict(name='do',
                 title='Do things',
                 archive_count=0,
                 description='Get things done'), self.board.json_reduce())

        vars = Vars()
        self.assertEqual(
            dict(generation=vars.generation, board=self.board, site=self.site),
            self.board.updates(self.board_generation))
        self.assertTrue(vars.generation > self.board_generation)
Esempio n. 7
0
    def test_request_access(self, sendmail):
        with self.db.transaction() as conn:
            auth = conn.root.sites['localhost'].auth
            message = auth.request('*****@*****.**', 'Testy Tester',
                                   'http://localhost')
            self.assertEqual("Your request is awaiting approval.", message)
            self.assertEqual([], sendmail.mock_calls)
            self.assertEqual(0, len(auth.users_by_uid))
            self.assertEqual(0, len(auth.users_by_email))
            self.assertEqual(2, len(auth.invites))
            user = auth.invites['*****@*****.**']  # email was normalized
            self.assertEqual(
                dict(id=Vars().id,
                     email='*****@*****.**',
                     name="Testy Tester",
                     nick='',
                     admin=False), user.data)
            self.assertEqual(False, user.approved)
            self.assertEqual(0, user.resets)

            # Additional calls have no effect:
            message = auth.request('*****@*****.**', 'Testy Tester',
                                   'http://localhost')
            self.assertEqual("Your request is awaiting approval.", message)
            self.assertEqual([], sendmail.mock_calls)
            self.assertEqual(0, len(auth.users_by_uid))
            self.assertEqual(0, len(auth.users_by_email))
            self.assertEqual(2, len(auth.invites))
            user = auth.invites['*****@*****.**']
            self.assertEqual(
                dict(id=Vars().id,
                     email='*****@*****.**',
                     name="Testy Tester",
                     nick='',
                     admin=False), user.data)
            self.assertEqual(False, user.approved)
            self.assertEqual(0, user.resets)
Esempio n. 8
0
    def test_update_invalid(self):
        self.board.new_project('first', 42, 'Do First thing')
        vars = Vars()
        self.assertEqual(
            dict(generation=vars.board_generation,
                 tasks=dict(adds=[vars.project])),
            self.board.updates(self.board_generation))

        with self.assertRaises(Exception):
            self.board.update_task(vars.project.id, order='42')

        with self.assertRaises(Exception):
            self.board.update_task(vars.project.id, foo=1)

        self.assertEqual(self.board.generation, vars.board_generation)
Esempio n. 9
0
 def test_rename_board(self):
     with self._app.database.transaction() as conn:
         site = get_site(conn.root, 'localhost')
         site.add_board('t')
         site.add_board('tt')
     self.get('/board/t/poll')
     site_app = self._test_app('/site/poll')
     tt_app = self._test_app('/site/poll')
     r = self.put('/board/t/', dict(name='t2'))
     vars = Vars()
     self.assertEqual(dict(board=vars.board, site=vars.site,
                           generation=vars.g),
                      r.json['updates'])
     self.assertEqual(['t2', 'tt'],
                      [b['name'] for b in vars.site['boards']])
     self.assertEqual('t2', vars.board['name'])
Esempio n. 10
0
    def test_move_into_project_wo_changing_state_or_order(self):
        self.board.new_project('first', 42, 'Do First thing')
        vars = Vars()
        self.assertEqual(
            dict(generation=vars.gen1, tasks=dict(adds=[vars.t1])),
            self.board.updates(self.board_generation))

        self.board.new_project('second', 43, 'Do Next thing')
        self.assertEqual(
            dict(generation=vars.gen2, tasks=dict(adds=[vars.t2])),
            self.board.updates(vars.gen1))
        self.assertTrue(vars.gen2 > vars.gen1)

        self.board.new_project('third', 44, 'Do 3rd thing')
        self.assertEqual(
            dict(generation=vars.gen3, tasks=dict(adds=[vars.t3])),
            self.board.updates(vars.gen2))
        self.assertTrue(vars.gen3 > vars.gen2)

        # Make second project a task
        self.board.move(vars.t2.id, vars.t1.id, user_id='test')
        self.assertEqual(vars.t1.id, vars.t2.parent.id)
        self.assertEqual('ready', vars.t2.state.id)
        self.assertEqual(43, vars.t2.order)

        # Move t2 to an actual state:
        self.board.move(vars.t2.id,
                        vars.t1.id,
                        self._state_id('Doing'),
                        user_id='test')

        # Move t2 to t3, and state and order are preserved:
        self.board.move(vars.t2.id, vars.t3.id, user_id='test')
        self.assertEqual(vars.t3.id, vars.t2.parent.id)
        self.assertEqual(self._state_id('Doing'), vars.t2.state.id)
        self.assertEqual(43, vars.t2.order)

        # Move t1 to new state then move to p3 and state is lost:
        self.board.move(vars.t1.id,
                        state_id=self._state_id('Development'),
                        user_id='test')
        self.board.move(vars.t1.id, vars.t3.id, user_id='test')
        self.assertEqual(vars.t3.id, vars.t1.parent.id)
        self.assertEqual('ready', vars.t1.state.id)
        self.assertEqual(42, vars.t1.order)
Esempio n. 11
0
    def test_new_task(self):
        vars = Vars()
        self.board.new_project('first', 42, 'Do First thing')
        self.assertEqual(
            dict(generation=vars.board_generation,
                 tasks=dict(adds=[vars.project])),
            self.board.updates(self.board_generation))

        self.board.new_task(vars.project.id, 'a sub', 43, 'A First subtask')
        self.assertEqual(
            dict(generation=vars.generation, tasks=dict(adds=[vars.task])),
            self.board.updates(vars.board_generation))
        self.assertTrue(vars.generation > vars.board_generation)

        self.assertEqual('a sub', vars.task.title)
        self.assertEqual(43, vars.task.order)
        self.assertEqual('A First subtask', vars.task.description)
        self.assertEqual(vars.project, vars.task.parent)
Esempio n. 12
0
    def test_add_board(self):
        vars = Vars()
        generation = self.site.generation
        self.site.add_board('first', 'The first one', 'Yup, the first')
        self.assertEqual([('first', vars.board)],
                         list(self.site.boards.items()))
        self.assertEqual(self.site, vars.board.site)
        self.assertEqual('first', vars.board.name)
        self.assertEqual('The first one', vars.board.title)
        self.assertEqual('Yup, the first', vars.board.description)
        self.assertTrue(self.site.generation > generation)

        generation = vars.board.generation
        self.site.add_board('second', 'The second one', 'Yup, the second')
        self.assertEqual(['first', 'second'], list(self.site.boards))

        # The original board was updated:
        self.assertTrue(vars.board.generation > generation)
Esempio n. 13
0
    def test_web_update_profile(self):
        db = self.app.database
        from .. import emailpw
        with db.transaction() as conn:
            user = emailpw.User('*****@*****.**', 'Tester', 'tester')
            user.setpw(b'secret')
            epw = conn.root.sites['localhost'].auth
            epw.users_by_uid[user.id] = user
            epw.users_by_email[user.email] = user

        app = self._test_app()
        self._login(app, user.email, 'secret')

        # Now, we'll updata the user's profile
        r = app.put_json('/auth/user',
                         dict(name='name', nick='nick', email='*****@*****.**'))
        vars = Vars()
        self.assertEqual(
            {
                'updates': {
                    'generation': vars.generation,
                    'site': {
                        'boards': [],
                        'users': [{
                            'admin': False,
                            'email': '*****@*****.**',
                            'id': vars.id,
                            'name': 'name',
                            'nick': 'nick'
                        }]
                    },
                    'user': {
                        'admin': False,
                        'email': '*****@*****.**',
                        'id': vars.id,
                        'name': 'name',
                        'nick': 'nick'
                    },
                    'zoid': vars.zoid
                }
            }, r.json)
Esempio n. 14
0
    def test_remove(self):
        vars = Vars()
        with self._app.database.transaction() as conn:
            site = get_site(conn.root, 'localhost')
            site.add_board('test', '', '')
            board = site.boards['test']
            board.new_project('p1', 0)
            [p1id] = [t.id for t in board.tasks]

        self.get('/board/test/poll') # set generation

        r = self.app.delete('/board/test/tasks/' + p1id)
        updates = r.json['updates']
        self.assertEqual(dict(generation=vars.generation,
                              tasks=dict(removals=[p1id])),
                         r.json['updates'])

        with self._app.database.transaction() as conn:
            site = get_site(conn.root, 'localhost')
            board = site.boards['test']
            self.assertEqual(0, len(board.tasks))
Esempio n. 15
0
    def test_update_all(self):
        self.board.new_project('first', 42, 'Do First thing')
        vars = Vars()
        self.assertEqual(
            dict(generation=vars.board_generation,
                 tasks=dict(adds=[vars.project])),
            self.board.updates(self.board_generation))

        data = dict(
            title='a project',
            description='It will be great',
            size=2,
            blocked="It's in the way",
            assigned='j1m',
        )

        self.board.update_task(vars.project.id, **data)
        self.assertEqual(
            dict(generation=vars.generation, tasks=dict(adds=[vars.project])),
            self.board.updates(vars.board_generation))
        self.assertTrue(vars.generation > vars.board_generation)
Esempio n. 16
0
 def test_bootstrap(self):
     (to, subject, message), _ = self._sent
     self.assertEqual("Jaci Admi <*****@*****.**>", to)
     self.assertEqual("Set your password for Test site", subject)
     self.assertTrue(
         message.strip().startswith("Your request to use Test site"))
     with self.db.transaction() as conn:
         auth = conn.root.sites['localhost'].auth
         self.assertEqual(0, len(auth.users_by_uid))
         self.assertEqual(0, len(auth.users_by_email))
         vars = Vars()
         self.assertEqual([('*****@*****.**', vars.user)],
                          list(auth.invites.items()))
         self.assertEqual(
             dict(id=vars.id,
                  email='*****@*****.**',
                  name="Jaci Admi",
                  nick='',
                  admin=True), vars.user.data)
         self.assertEqual(True, vars.user.approved)
         self.assertEqual(1, vars.user.resets)
Esempio n. 17
0
    def test_sanitize(self):
        self.board.new_project('first', 42, sample_description)
        vars = Vars()
        self.assertEqual(
            dict(generation=vars.board_generation,
                 tasks=dict(adds=[vars.project])),
            self.board.updates(self.board_generation))

        self.assertEqual(sample_description_cleaned, vars.project.description)

        self.board.new_task(vars.project.id, 'a sub', 43, sample_description)
        self.assertEqual(
            dict(generation=vars.generation, tasks=dict(adds=[vars.task])),
            self.board.updates(vars.board_generation))
        self.assertTrue(vars.generation > vars.board_generation)

        self.assertEqual(sample_description_cleaned, vars.task.description)

        self.board.update_task(vars.task.id, description='')
        self.assertEqual('', vars.task.description)
        self.board.update_task(vars.task.id, description=sample_description)
        self.assertEqual(sample_description_cleaned, vars.task.description)
Esempio n. 18
0
    def test_request_approved_user(self, sendmail, log_error):
        with self.db.transaction() as conn:
            auth = conn.root.sites['localhost'].auth
            for i in range(11):
                message = auth.request('*****@*****.**', 'Testy Tester',
                                       'http://localhost')
                self.assertEqual(
                    "An email has been sent to [email protected]"
                    " with a link to set your password", message)

                if i < 8:
                    self.assertEqual(i + 1, len(sendmail.mock_calls))
                    self.assertEqual(0, len(log_error.mock_calls))

                    (to, subject, message), _ = sendmail.call_args
                    self.assertEqual("Jaci Admi <*****@*****.**>", to)
                    self.assertEqual("Set your password for Test site",
                                     subject)
                    self.assertTrue(message.strip().startswith(
                        "Your request to use Test site"))
                else:
                    self.assertEqual(8, len(sendmail.mock_calls))
                    self.assertEqual(i - 7, len(log_error.mock_calls))

                self.assertEqual(0, len(auth.users_by_uid))
                self.assertEqual(0, len(auth.users_by_email))
                vars = Vars()
                self.assertEqual([('*****@*****.**', vars.user)],
                                 list(auth.invites.items()))
                self.assertEqual(
                    dict(id=vars.id,
                         email='*****@*****.**',
                         name="Jaci Admi",
                         nick='',
                         admin=True), vars.user.data)
                self.assertEqual(True, vars.user.approved)
                self.assertEqual(i + 2, vars.user.resets)
Esempio n. 19
0
    def test_archive_and_restore_empty_feature(self, now):
        now.return_value = '2017-06-08T10:02:00.004'
        board, updates = self.board, self.updates
        vars = Vars()

        # Make an empty feature:
        board.new_project('p1', 42, '')
        self.assertEqual([vars.p1], updates()['tasks']['adds'])

        # Now, archive p1:
        self.assertEqual(0, board.archive_count)
        now.return_value = '2017-06-08T10:02:01.004'
        board.archive_feature(vars.p1.id)
        self.assertEqual(
            dict(board=board,
                 site=board.site,
                 tasks=dict(removals=[vars.p1.id])), updates())
        self.assertEqual(1, board.archive_count)

        # If we asked for all updates, we'd see the expected data:
        self.assertEqual(
            dict(
                board=board,
                site=board.site,
                states=vars.states,
                tasks={},
                generation=vars.generation,
                zoid=vars.zoid,
            ), board.updates(0))

        # p1, which is now archived, has it's tasks in it's tasks attr:
        self.assertEqual([], vars.p1.tasks)

        # History has the archival event:
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:01.004',
                state="Backlog",
                archived=True,
            ), vars.p1.history[-1])
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:00.004',
                end='2017-06-08T10:02:01.004',
                state="Backlog",
            ), vars.p1.history[-2])

        # OK, now let's retore:
        now.return_value = '2017-06-08T10:02:02.004'
        board.restore_feature(vars.p1.id)
        self.assertEqual(
            dict(board=board, site=board.site, tasks=dict(adds=[vars.p1])),
            updates())
        self.assertEqual(0, board.archive_count)

        # If we asked for all updates, we'd see the expected data:
        self.assertEqual(
            dict(
                board=board,
                site=board.site,
                states=vars.states,
                tasks=dict(adds=[vars.p1]),
                generation=vars.generationr,
                zoid=vars.zoid,
            ), board.updates(0))

        # History has the archival event:
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:02.004',
                state="Backlog",
            ), vars.p1.history[-1])
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:01.004',
                end='2017-06-08T10:02:02.004',
                state="Backlog",
                archived=True,
            ), vars.p1.history[-2])
Esempio n. 20
0
    def test_archive_and_restore(self, now):
        now.return_value = '2017-06-08T10:02:00.004'
        board, updates = self.board, self.updates
        vars = Vars()

        # Make some features:
        board.new_project('p1', 42, '')
        self.assertEqual([vars.p1], updates()['tasks']['adds'])
        board.new_task(vars.p1.id, 't1', 1)
        self.assertEqual([vars.t1], updates()['tasks']['adds'])
        board.new_task(vars.p1.id, 't2', 1)
        self.assertEqual([vars.t2], updates()['tasks']['adds'])

        # Decoys to make sure we don't work on too much:
        board.new_project('p2', 43, '')
        self.assertEqual([vars.p2], updates()['tasks']['adds'])
        board.new_task(vars.p2.id, 't3', 1)
        self.assertEqual([vars.t3], updates()['tasks']['adds'])

        # Move p1 to a state other than backlog
        board.move(vars.p1.id, state_id='Development')
        updates()  # reset generation

        # Now, archive p1:
        self.assertEqual(0, board.archive_count)
        now.return_value = '2017-06-08T10:02:01.004'
        board.archive_feature(vars.p1.id)
        self.assertEqual(
            dict(board=board,
                 site=board.site,
                 tasks=dict(removals=vars.removals)), updates())
        self.assertEqual(1, board.archive_count)

        # If we asked for all updates, we'd see the expected data:
        self.assertEqual(
            dict(
                board=board,
                site=board.site,
                states=vars.states,
                tasks=dict(adds=vars.remaining),
                generation=vars.generation,
                zoid=vars.zoid,
            ), board.updates(0))
        self.assertEqual(['p2', 't3'], sorted(t.title for t in vars.remaining))

        # p1, which is now archived, has it's tasks in it's tasks attr:
        self.assertEqual([vars.t1, vars.t2],
                         sorted(vars.p1.tasks, key=lambda t: t.title))

        # History has the archival event:
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:01.004',
                state="Development",
                working=True,
                archived=True,
            ), vars.p1.history[-1])
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:00.004',
                end='2017-06-08T10:02:01.004',
                state="Development",
                working=True,
            ), vars.p1.history[-2])

        # OK, now let's retore:
        now.return_value = '2017-06-08T10:02:02.004'
        board.restore_feature(vars.p1.id)
        self.assertEqual(
            dict(board=board, site=board.site, tasks=dict(adds=vars.restored)),
            updates())
        self.assertEqual(0, board.archive_count)
        self.assertEqual([vars.p1, vars.t1, vars.t2],
                         sorted(vars.restored, key=lambda t: t.title))

        # If we asked for all updates, we'd see the expected data:
        self.assertEqual(
            dict(
                board=board,
                site=board.site,
                states=vars.states,
                tasks=dict(adds=vars.all),
                generation=vars.generationr,
                zoid=vars.zoid,
            ), board.updates(0))
        self.assertEqual([vars.p1, vars.p2, vars.t1, vars.t2, vars.t3],
                         sorted(vars.all, key=lambda t: t.title))

        # History has the archival event:
        self.assertEqual(
            dict(start='2017-06-08T10:02:02.004',
                 state="Development",
                 working=True), vars.p1.history[-1])
        self.assertEqual(
            dict(
                start='2017-06-08T10:02:01.004',
                end='2017-06-08T10:02:02.004',
                state="Development",
                working=True,
                archived=True,
            ), vars.p1.history[-2])
Esempio n. 21
0
    def test_initial_data(self):
        vars = Vars()
        self.assertEqual(
            dict(
                generation=self.board_generation,
                states=dict(adds=vars.states),
                board=self.board,
                site=self.site,
                zoid=str(u64(self.board.changes._p_oid)),
            ), self.board.updates(0))

        self.assertEqual([{
            'complete': False,
            'explode': False,
            'id': 'Backlog',
            'order': 0,
            'task': False,
            'title': 'Backlog',
            'working': False
        }, {
            'complete': False,
            'explode': False,
            'id': 'Ready',
            'order': 1,
            'task': False,
            'title': 'Ready',
            'working': False
        }, {
            'complete': False,
            'explode': True,
            'id': 'Development',
            'order': 2,
            'task': False,
            'title': 'Development',
            'working': True
        }, {
            'complete': False,
            'explode': False,
            'id': 'ready',
            'order': 3,
            'task': True,
            'title': 'Ready',
            'working': False
        }, {
            'complete': False,
            'explode': False,
            'id': 'Doing',
            'order': 4,
            'task': True,
            'title': 'Doing',
            'working': True
        }, {
            'complete': False,
            'explode': False,
            'id': 'Needs review',
            'order': 5,
            'task': True,
            'title': 'Needs review',
            'working': False
        }, {
            'complete': False,
            'explode': False,
            'id': 'Review',
            'order': 6,
            'task': True,
            'title': 'Review',
            'working': True
        }, {
            'complete': True,
            'explode': False,
            'id': 'Done',
            'order': 7,
            'task': True,
            'title': 'Done',
            'working': False
        }, {
            'complete': False,
            'explode': False,
            'id': 'Acceptance',
            'order': 8,
            'task': False,
            'title': 'Acceptance',
            'working': True
        }, {
            'complete': False,
            'explode': False,
            'id': 'Deploying',
            'order': 9,
            'task': False,
            'title': 'Deploying',
            'working': True
        }, {
            'complete': False,
            'explode': False,
            'id': 'Deployed',
            'order': 10,
            'task': False,
            'title': 'Deployed',
            'working': False
        }], reduce(vars.states))

        self.assertEqual(len(set(state.id for state in vars.states)),
                         len(vars.states))
Esempio n. 22
0
    def test_move(self, now):
        now.return_value = '2017-06-08T10:02:00.004'
        self.board.new_project('first', 42, 'Do First thing')
        vars = Vars()
        self.assertEqual(
            dict(generation=vars.gen1, tasks=dict(adds=[vars.t1])),
            self.board.updates(self.board_generation))

        self.board.new_project('second', 43, 'Do Next thing')
        self.assertEqual(
            dict(generation=vars.gen2, tasks=dict(adds=[vars.t2])),
            self.board.updates(vars.gen1))
        self.assertTrue(vars.gen2 > vars.gen1)

        self.board.new_project('third', 44, 'Do 3rd thing')
        self.assertEqual(
            dict(generation=vars.gen3, tasks=dict(adds=[vars.t3])),
            self.board.updates(vars.gen2))
        self.assertTrue(vars.gen3 > vars.gen2)

        self.assertEqual(({
            'start': '2017-06-08T10:02:00.004',
            'state': 'Backlog'
        }, ), vars.t1.history)
        self.assertEqual(({
            'start': '2017-06-08T10:02:00.004',
            'state': 'Backlog'
        }, ), vars.t2.history)
        self.assertEqual(({
            'start': '2017-06-08T10:02:00.004',
            'state': 'Backlog'
        }, ), vars.t3.history)

        # Make second project a task
        now.return_value = '2017-06-08T10:02:01.004'
        self.board.move(vars.t2.id,
                        vars.t1.id,
                        self._state_id('Doing'),
                        1,
                        user_id='test')
        self.assertEqual(
            dict(generation=vars.gen4, tasks=dict(adds=[vars.t2])),
            self.board.updates(vars.gen3))
        self.assertTrue(vars.gen4 > vars.gen3)

        self.assertEqual(vars.t1, vars.t2.parent)
        self.assertEqual('Doing', vars.t2.state.title)
        self.assertEqual(1, vars.t2.order)

        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:01.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:01.004",
                'state': "Doing",
                'assigned': 'test'
            },
        ), vars.t2.history)

        # Move first project to new state
        now.return_value = '2017-06-08T10:02:02.004'
        self.board.move(vars.t1.id,
                        None,
                        self._state_id('Development'),
                        2,
                        user_id='test')
        self.assertEqual(
            dict(generation=vars.gen5, tasks=dict(adds=[vars.t1, vars.t2])),
            self.board.updates(vars.gen4))
        self.assertTrue(vars.gen5 > vars.gen4)

        self.assertEqual(None, vars.t1.parent)
        self.assertEqual('Development', vars.t1.state.title)
        self.assertEqual(2, vars.t1.order)
        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:02.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:02.004",
                'state': "Development",
                'working': True
            },
        ), vars.t1.history)
        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:01.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:01.004",
                'end': "2017-06-08T10:02:02.004",
                'state': "Doing",
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:02.004",
                'state': "Doing",
                'working': True,
                'assigned': 'test'
            },
        ), vars.t2.history)

        # Change order of first
        self.board.move(vars.t1.id,
                        None,
                        self._state_id('Development'),
                        3,
                        user_id='test')
        self.assertEqual(
            dict(generation=vars.gen6, tasks=dict(adds=[vars.t1])),
            self.board.updates(vars.gen5))
        self.assertTrue(vars.gen6 > vars.gen5)

        self.assertEqual(None, vars.t1.parent)
        self.assertEqual('Development', vars.t1.state.title)
        self.assertEqual(3, vars.t1.order)

        ##################################################################
        # Same as prev
        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:02.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:02.004",
                'state': "Development",
                'working': True
            },
        ), vars.t1.history)
        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:01.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:01.004",
                'end': "2017-06-08T10:02:02.004",
                'state': "Doing",
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:02.004",
                'state': "Doing",
                'working': True,
                'assigned': 'test'
            },
        ), vars.t2.history)
        #
        ##################################################################

        # Now, some invalid moves:

        # Can't move to task:
        with self.assertRaises(Exception):
            self.board.move(vars.t3.id,
                            vars.t2.id,
                            self._state_id('Doing'),
                            4,
                            user_id='test')

        # Can't make non-empty feature a task:
        with self.assertRaises(Exception):
            self.board.move(vars.t1.id,
                            vars.t3.id,
                            self._state_id('Doing'),
                            4,
                            user_id='test')

        # Can't move to feature with task state:
        with self.assertRaises(Exception):
            self.board.move(vars.t3.id,
                            None,
                            self._state_id('Doing'),
                            4,
                            user_id='test')

        # Can't move to project with to-level state:
        with self.assertRaises(Exception):
            self.board.move(vars.t3.id, vars.t1.id, self._state_id('Backlog'),
                            4)

        # finish t2
        now.return_value = '2017-06-08T10:02:03.004'
        self.board.move(vars.t2.id,
                        vars.t1.id,
                        self._state_id('Done'),
                        1,
                        user_id='test')
        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:01.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:01.004",
                'end': "2017-06-08T10:02:02.004",
                'state': "Doing",
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:02.004",
                'end': "2017-06-08T10:02:03.004",
                'state': "Doing",
                'working': True,
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:03.004",
                'state': "Done",
                'complete': True,
                'assigned': 'test'
            },
        ), vars.t2.history)

        # We can promote a task to a project:
        now.return_value = '2017-06-08T10:02:04.004'
        self.board.move(vars.t2.id,
                        None,
                        self._state_id('Backlog'),
                        5,
                        user_id='test')
        self.assertEqual(
            dict(generation=vars.gen7, tasks=dict(adds=[vars.t2])),
            self.board.updates(vars.gen6))
        self.assertTrue(vars.gen7 > vars.gen6)

        self.assertEqual(None, vars.t2.parent)
        self.assertEqual('Backlog', vars.t2.state.title)
        self.assertEqual(5, vars.t2.order)
        self.assertEqual((
            {
                'start': "2017-06-08T10:02:00.004",
                'end': "2017-06-08T10:02:01.004",
                'state': 'Backlog'
            },
            {
                'start': "2017-06-08T10:02:01.004",
                'end': "2017-06-08T10:02:02.004",
                'state': "Doing",
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:02.004",
                'end': "2017-06-08T10:02:03.004",
                'state': "Doing",
                'working': True,
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:03.004",
                'end': "2017-06-08T10:02:04.004",
                'state': "Done",
                'complete': True,
                'assigned': 'test'
            },
            {
                'start': "2017-06-08T10:02:04.004",
                'state': "Backlog",
            },
        ), vars.t2.history)