예제 #1
0
    def test_round_trip_with_client_wins(self):
        # Load some data
        cmd = 'kinto-wizard {} --server={} --auth={}'
        load_cmd = cmd.format("load {}".format(self.file), self.server,
                              self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        # Change something that could make the server to fail.
        client = Client(server_url=self.server,
                        auth=tuple(self.auth.split(':')))
        client.update_record(bucket='build-hub',
                             collection='archives',
                             id='0831d549-0a69-48dd-b240-feef94688d47',
                             data={})
        record = client.get_record(bucket='build-hub',
                                   collection='archives',
                                   id='0831d549-0a69-48dd-b240-feef94688d47')
        assert set(record['data'].keys()) == {'id', 'last_modified'}
        cmd = 'kinto-wizard {} --server={} -D --auth={} --force'
        load_cmd = cmd.format("load {}".format(self.file), self.server,
                              self.auth)
        sys.argv = load_cmd.split(" ")
        main()
        record = client.get_record(bucket='build-hub',
                                   collection='archives',
                                   id='0831d549-0a69-48dd-b240-feef94688d47')
        assert set(record['data'].keys()) != {'id', 'last_modified'}
예제 #2
0
    def test_collection_argument_takes_precedence(self):
        mock_response(self.session)
        # Specify a different collection name for the client and the operation.
        client = Client(session=self.session, bucket='mybucket',
                        collection='wrong_collection')
        client.update_record(data={'id': '1234'}, collection='good_collection',
                             permissions=mock.sentinel.permissions)

        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/good_collection/records/1234',
            data={'id': '1234'},
            headers=None,
            permissions=mock.sentinel.permissions)
예제 #3
0
    def test_collection_argument_takes_precedence(self):
        mock_response(self.session)
        # Specify a different collection name for the client and the operation.
        client = Client(session=self.session, bucket='mybucket',
                        collection='wrong_collection')
        client.update_record(data={'id': '1234'}, collection='good_collection',
                             permissions=mock.sentinel.permissions)

        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/good_collection/records/1234',
            data={'id': '1234'},
            headers=None,
            permissions=mock.sentinel.permissions)
예제 #4
0
    def test_single_record_save(self):
        client = Client(server_url=self.server_url, auth=self.auth,
                        bucket='mozilla', collection='payments')
        client.create_bucket()
        client.create_collection()
        created = client.create_record(data={'foo': 'bar'},
                                       permissions={'read': ['account:alexis']})
        created['data']['bar'] = 'baz'

        # XXX enhance this in order to have to pass only one argument, created.
        client.update_record(id=created['data']['id'], data=created['data'])

        retrieved = client.get_record(id=created['data']['id'])
        assert 'account:alexis' in retrieved['permissions']['read']
        assert retrieved['data']['foo'] == u'bar'
        assert retrieved['data']['bar'] == u'baz'
        assert created['data']['id'] == retrieved['data']['id']
예제 #5
0
    def test_single_record_save(self):
        client = Client(server_url=self.server_url, auth=self.auth,
                        bucket='mozilla', collection='payments')
        client.create_bucket()
        client.create_collection()
        created = client.create_record(data={'foo': 'bar'},
                                       permissions={'read': ['alexis']})
        created['data']['bar'] = 'baz'

        # XXX enhance this in order to have to pass only one argument, created.
        client.update_record(id=created['data']['id'], data=created['data'])

        retrieved = client.get_record(created['data']['id'])
        assert 'alexis' in retrieved['permissions']['read']
        assert retrieved['data']['foo'] == u'bar'
        assert retrieved['data']['bar'] == u'baz'
        assert created['data']['id'] == retrieved['data']['id']
예제 #6
0
    def test_single_record_save(self):
        client = Client(server_url=self.server_url,
                        auth=self.auth,
                        bucket="mozilla",
                        collection="payments")
        client.create_bucket()
        client.create_collection()
        created = client.create_record(
            data={"foo": "bar"}, permissions={"read": ["account:alexis"]})
        created["data"]["bar"] = "baz"

        # XXX enhance this in order to have to pass only one argument, created.
        client.update_record(id=created["data"]["id"], data=created["data"])

        retrieved = client.get_record(id=created["data"]["id"])
        assert "account:alexis" in retrieved["permissions"]["read"]
        assert retrieved["data"]["foo"] == u"bar"
        assert retrieved["data"]["bar"] == u"baz"
        assert created["data"]["id"] == retrieved["data"]["id"]
예제 #7
0
class RecordLoggingTest(unittest.TestCase):
    def setUp(self):
        self.session = mock.MagicMock()
        self.client = Client(session=self.session)
        mock_response(self.session)

    def test_create_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket(id='buck')
            self.client.create_collection(id='mozilla',
                                          bucket='buck')
            self.client.create_record(
                id='fake-record',
                data={'foo': 'bar'},
                permissions={'write': ['blah', ]},
                bucket='buck',
                collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Create record with id 'fake-record' in collection 'mozilla' in bucket 'buck'")

    def test_update_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket(id='buck')
            self.client.create_collection(bucket='buck',
                                          id='mozilla')
            self.client.update_record(
                id='fake-record',
                data={'ss': 'aa'},
                bucket='buck',
                collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Update record with id 'fake-record' in collection 'mozilla' in bucket 'buck'")

    def test_get_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket(id='buck')
            self.client.create_collection(id='mozilla',
                                          bucket='buck')
            self.client.get_record(
                id='fake-record',
                bucket='buck',
                collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Get record with id 'fake-record' from collection 'mozilla' in bucket 'buck'")

    def test_delete_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket(id='buck')
            self.client.create_collection(id='mozilla',
                                          bucket='buck')
            self.client.delete_record(
                id='fake-record',
                bucket='buck',
                collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Delete record with id 'fake-record' from collection 'mozilla' in bucket 'buck'")

    def test_delete_records_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket(id='buck')
            self.client.create_collection(id='mozilla',
                                          bucket='buck')
            self.client.delete_records(
                bucket='buck',
                collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Delete records from collection 'mozilla' in bucket 'buck'")
예제 #8
0
class RecordLoggingTest(unittest.TestCase):
    def setUp(self):
        self.session = mock.MagicMock()
        self.client = Client(session=self.session)
        mock_response(self.session)

    def test_create_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket('buck')
            self.client.create_collection('mozilla', bucket='buck')
            self.client.create_record(id='fake-record',
                                      data={'foo': 'bar'},
                                      permissions={'write': [
                                          'blah',
                                      ]},
                                      bucket='buck',
                                      collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Create record with id 'fake-record' in collection 'mozilla' in bucket 'buck'"
            )

    def test_update_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket('buck')
            self.client.create_collection('mozilla', bucket='buck')
            self.client.update_record(id='fake-record',
                                      data={'ss': 'aa'},
                                      bucket='buck',
                                      collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Update record with id 'fake-record' in collection 'mozilla' in bucket 'buck'"
            )

    def test_get_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket('buck')
            self.client.create_collection('mozilla', bucket='buck')
            self.client.get_record(id='fake-record',
                                   bucket='buck',
                                   collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Get record with id 'fake-record' from collection 'mozilla' in bucket 'buck'"
            )

    def test_delete_record_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket('buck')
            self.client.create_collection('mozilla', bucket='buck')
            self.client.delete_record(id='fake-record',
                                      bucket='buck',
                                      collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Delete record with id 'fake-record' from collection 'mozilla' in bucket 'buck'"
            )

    def test_delete_records_logs_info_message(self):
        with mock.patch('kinto_http.logger') as mocked_logger:
            self.client.create_bucket('buck')
            self.client.create_collection('mozilla', bucket='buck')
            self.client.delete_records(bucket='buck', collection='mozilla')
            mocked_logger.info.assert_called_with(
                "Delete records from collection 'mozilla' in bucket 'buck'")
예제 #9
0
class RecordTest(unittest.TestCase):
    def setUp(self):
        self.session = mock.MagicMock()
        self.client = Client(
            session=self.session, bucket='mybucket',
            collection='mycollection')

    def test_record_id_is_given_after_creation(self):
        mock_response(self.session, data={'id': 5678})
        record = self.client.create_record({'foo': 'bar'})
        assert 'id' in record['data'].keys()

    def test_generated_record_id_is_an_uuid(self):
        mock_response(self.session)
        self.client.create_record({'foo': 'bar'})
        id = self.session.request.mock_calls[0][1][1].split('/')[-1]

        uuid_regexp = r'[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}'
        self.assertRegexpMatches(id, uuid_regexp)

    def test_records_handles_permissions(self):
        mock_response(self.session)
        self.client.create_record(
            {'id': '1234', 'foo': 'bar'},
            permissions=mock.sentinel.permissions)
        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/mycollection/records/1234',
            data={'foo': 'bar', 'id': '1234'},
            permissions=mock.sentinel.permissions,
            headers=DO_NOT_OVERWRITE)

    def test_collection_argument_takes_precedence(self):
        mock_response(self.session)
        # Specify a different collection name for the client and the operation.
        client = Client(session=self.session, bucket='mybucket',
                        collection='wrong_collection')
        client.update_record(data={'id': '1234'}, collection='good_collection',
                             permissions=mock.sentinel.permissions)

        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/good_collection/records/1234',
            data={'id': '1234'},
            headers=None,
            permissions=mock.sentinel.permissions)

    def test_record_id_is_derived_from_data_if_present(self):
        mock_response(self.session)
        self.client.create_record(data={'id': '1234', 'foo': 'bar'},
                                  permissions=mock.sentinel.permissions)

        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/mycollection/records/1234',
            data={'id': '1234', 'foo': 'bar'},
            permissions=mock.sentinel.permissions,
            headers=DO_NOT_OVERWRITE)

    def test_data_and_permissions_are_added_on_create(self):
        mock_response(self.session)
        data = {'foo': 'bar'}
        permissions = {'read': ['mle']}

        self.client.create_record(
            id='1234',
            data=data,
            permissions=permissions)

        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with(
            'put', url, data=data, permissions=permissions,
            headers=DO_NOT_OVERWRITE)

    def test_creation_sends_if_none_match_by_default(self):
        mock_response(self.session)
        data = {'foo': 'bar'}

        self.client.create_record(
            id='1234',
            data=data)

        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with(
            'put', url, data=data, permissions=None, headers=DO_NOT_OVERWRITE)

    def test_creation_doesnt_add_if_none_match_when_overwrite(self):
        mock_response(self.session)
        data = {'foo': 'bar'}

        self.client.create_record(id='1234', data=data, safe=False)

        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with(
            'put', url, data=data, permissions=None, headers=None)

    def test_records_issues_a_request_on_delete(self):
        mock_response(self.session)
        self.client.delete_record('1234')
        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with('delete', url, headers=None)

    def test_record_issues_a_request_on_retrieval(self):
        mock_response(self.session, data={'foo': 'bar'})
        record = self.client.get_record('1234')

        self.assertEquals(record['data'], {'foo': 'bar'})
        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with('get', url)

    def test_collection_can_retrieve_all_records(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}])
        records = self.client.get_records()
        assert list(records) == [{'id': 'foo'}, {'id': 'bar'}]

    def test_collection_can_retrieve_records_timestamp(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"12345"'})
        timestamp = self.client.get_records_timestamp()
        assert timestamp == '12345'

    def test_records_timestamp_is_cached(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"12345"'})
        self.client.get_records()
        timestamp = self.client.get_records_timestamp()
        assert timestamp == '12345'
        assert self.session.request.call_count == 1

    def test_records_timestamp_is_cached_per_collection(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"12345"'})
        self.client.get_records(collection="foo")
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"67890"'})
        self.client.get_records(collection="bar")

        timestamp = self.client.get_records_timestamp("foo")
        assert timestamp == '12345'

        timestamp = self.client.get_records_timestamp("bar")
        assert timestamp == '67890'

    def test_pagination_is_followed(self):
        # Mock the calls to request.
        link = ('http://example.org/buckets/buck/collections/coll/records/'
                '?token=1234')

        self.session.request.side_effect = [
            # First one returns a list of items with a pagination token.
            build_response(
                [{'id': '1', 'value': 'item1'},
                 {'id': '2', 'value': 'item2'}, ],
                {'Next-Page': link}),
            # Second one returns a list of items without a pagination token.
            build_response(
                [{'id': '3', 'value': 'item3'},
                 {'id': '4', 'value': 'item4'}, ],
            ),
        ]
        records = self.client.get_records('bucket', 'collection')

        assert list(records) == [
            {'id': '1', 'value': 'item1'},
            {'id': '2', 'value': 'item2'},
            {'id': '3', 'value': 'item3'},
            {'id': '4', 'value': 'item4'},
        ]

    def test_pagination_supports_if_none_match(self):
        link = ('http://example.org/buckets/buck/collections/coll/records/'
                '?token=1234')

        self.session.request.side_effect = [
            # First one returns a list of items with a pagination token.
            build_response(
                [{'id': '1', 'value': 'item1'},
                 {'id': '2', 'value': 'item2'}, ],
                {'Next-Page': link}),
            # Second one returns a list of items without a pagination token.
            build_response(
                [{'id': '3', 'value': 'item3'},
                 {'id': '4', 'value': 'item4'}, ],
            ),
        ]
        self.client.get_records('bucket', 'collection',
                                if_none_match="1234")

        # Check that the If-None-Match header is present in the requests.
        self.session.request.assert_any_call(
            'get', '/buckets/collection/collections/bucket/records',
            headers={'If-None-Match': '"1234"'}, params={})
        self.session.request.assert_any_call(
            'get', link, headers={'If-None-Match': '"1234"'}, params={})

    def test_collection_can_delete_a_record(self):
        mock_response(self.session, data={'id': 1234})
        resp = self.client.delete_record(id=1234)
        assert resp == {'id': 1234}
        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with('delete', url, headers=None)

    def test_record_delete_if_match(self):
        data = {}
        mock_response(self.session, data=data)
        deleted = self.client.delete_record(
            collection='mycollection',
            bucket='mybucket',
            id='1',
            if_match=1234)
        assert deleted == data
        url = '/buckets/mybucket/collections/mycollection/records/1'
        self.session.request.assert_called_with(
            'delete', url, headers={'If-Match': '"1234"'})

    def test_record_delete_if_match_not_included_if_not_safe(self):
        data = {}
        mock_response(self.session, data=data)
        deleted = self.client.delete_record(
            collection='mycollection',
            bucket='mybucket',
            id='1',
            if_match=1234,
            safe=False)
        assert deleted == data
        url = '/buckets/mybucket/collections/mycollection/records/1'
        self.session.request.assert_called_with(
            'delete', url, headers=None)

    def test_update_record_gets_the_id_from_data_if_exists(self):
        mock_response(self.session)
        self.client.update_record(
            bucket='mybucket', collection='mycollection',
            data={'id': 1, 'foo': 'bar'})

        self.session.request.assert_called_with(
            'put', '/buckets/mybucket/collections/mycollection/records/1',
            data={'id': 1, 'foo': 'bar'}, headers=None, permissions=None)

    def test_update_record_handles_if_match(self):
        mock_response(self.session)
        self.client.update_record(
            bucket='mybucket', collection='mycollection',
            data={'id': 1, 'foo': 'bar'}, if_match=1234)

        headers = {'If-Match': '"1234"'}
        self.session.request.assert_called_with(
            'put', '/buckets/mybucket/collections/mycollection/records/1',
            data={'id': 1, 'foo': 'bar'}, headers=headers, permissions=None)

    def test_patch_record_uses_the_patch_method(self):
        mock_response(self.session)
        self.client.patch_record(
            bucket='mybucket', collection='mycollection',
            data={'id': 1, 'foo': 'bar'})

        self.session.request.assert_called_with(
            'patch', '/buckets/mybucket/collections/mycollection/records/1',
            data={'id': 1, 'foo': 'bar'}, headers=None, permissions=None)

    def test_update_record_raises_if_no_id_is_given(self):
        with self.assertRaises(KeyError) as cm:
            self.client.update_record(
                data={'foo': 'bar'},  # Omit the id on purpose here.
                bucket='mybucket',
                collection='mycollection'
            )
        assert text_type(cm.exception) == (
            "'Unable to update a record, need an id.'")

    def test_get_or_create_doesnt_raise_in_case_of_conflict(self):
        data = {
            'permissions': mock.sentinel.permissions,
            'data': {'foo': 'bar'}
        }
        self.session.request.side_effect = [
            get_http_error(status=412),
            (data, None)
        ]
        returned_data = self.client.create_record(
            bucket="buck",
            collection="coll",
            data={'id': 1234,
                  'foo': 'bar'},
            if_not_exists=True)  # Should not raise.
        assert returned_data == data

    def test_get_or_create_raise_in_other_cases(self):
        self.session.request.side_effect = get_http_error(status=500)
        with self.assertRaises(KintoException):
            self.client.create_record(
                bucket="buck",
                collection="coll",
                data={'foo': 'bar'},
                if_not_exists=True)

    def test_create_record_raises_a_special_error_on_403(self):
        self.session.request.side_effect = get_http_error(status=403)
        with self.assertRaises(KintoException) as e:
            self.client.create_record(
                bucket="buck",
                collection="coll",
                data={'foo': 'bar'})
        expected_msg = ("Unauthorized. Please check that the collection exists"
                        " and that you have the permission to create or write "
                        "on this collection record.")
        assert e.exception.message == expected_msg
예제 #10
0
class RecordTest(unittest.TestCase):
    def setUp(self):
        self.session = mock.MagicMock()
        self.session.request.return_value = (mock.sentinel.response, mock.sentinel.count)
        self.client = Client(
            session=self.session, bucket='mybucket',
            collection='mycollection')

    def test_record_id_is_given_after_creation(self):
        mock_response(self.session, data={'id': 5678})
        record = self.client.create_record(data={'foo': 'bar'})
        assert 'id' in record['data'].keys()

    def test_generated_record_id_is_an_uuid(self):
        mock_response(self.session)
        self.client.create_record(data={'foo': 'bar'})
        id = self.session.request.mock_calls[0][1][1].split('/')[-1]

        uuid_regexp = r'[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}'
        self.assertRegex(id, uuid_regexp)

    def test_records_handles_permissions(self):
        mock_response(self.session)
        self.client.create_record(data={'id': '1234', 'foo': 'bar'},
                                  permissions=mock.sentinel.permissions)
        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/mycollection/records/1234',
            data={'foo': 'bar', 'id': '1234'},
            permissions=mock.sentinel.permissions,
            headers=DO_NOT_OVERWRITE)

    def test_collection_argument_takes_precedence(self):
        mock_response(self.session)
        # Specify a different collection name for the client and the operation.
        client = Client(session=self.session, bucket='mybucket',
                        collection='wrong_collection')
        client.update_record(data={'id': '1234'}, collection='good_collection',
                             permissions=mock.sentinel.permissions)

        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/good_collection/records/1234',
            data={'id': '1234'},
            headers=None,
            permissions=mock.sentinel.permissions)

    def test_record_id_is_derived_from_data_if_present(self):
        mock_response(self.session)
        self.client.create_record(data={'id': '1234', 'foo': 'bar'},
                                  permissions=mock.sentinel.permissions)

        self.session.request.assert_called_with(
            'put',
            '/buckets/mybucket/collections/mycollection/records/1234',
            data={'id': '1234', 'foo': 'bar'},
            permissions=mock.sentinel.permissions,
            headers=DO_NOT_OVERWRITE)

    def test_data_and_permissions_are_added_on_create(self):
        mock_response(self.session)
        data = {'foo': 'bar'}
        permissions = {'read': ['mle']}

        self.client.create_record(id='1234',
                                  data=data,
                                  permissions=permissions)

        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with(
            'put', url, data=data, permissions=permissions,
            headers=DO_NOT_OVERWRITE)

    def test_creation_sends_if_none_match_by_default(self):
        mock_response(self.session)
        data = {'foo': 'bar'}

        self.client.create_record(id='1234', data=data)

        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with(
            'put', url, data=data, permissions=None, headers=DO_NOT_OVERWRITE)

    def test_creation_doesnt_add_if_none_match_when_overwrite(self):
        mock_response(self.session)
        data = {'foo': 'bar'}

        self.client.create_record(id='1234', data=data, safe=False)

        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with(
            'put', url, data=data, permissions=None, headers=None)

    def test_records_issues_a_request_on_delete(self):
        mock_response(self.session)
        self.client.delete_record(id='1234')
        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with('delete', url, headers=None)

    def test_record_issues_a_request_on_retrieval(self):
        mock_response(self.session, data={'foo': 'bar'})
        record = self.client.get_record(id='1234')

        self.assertEqual(record['data'], {'foo': 'bar'})
        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with('get', url)

    def test_collection_can_retrieve_all_records(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}])
        records = self.client.get_records()
        assert list(records) == [{'id': 'foo'}, {'id': 'bar'}]

    def test_collection_can_retrieve_records_timestamp(self):
        mock_response(self.session, headers={"ETag": '"12345"'})
        timestamp = self.client.get_records_timestamp()
        assert timestamp == '12345'

    def test_records_timestamp_is_cached(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"12345"'})
        self.client.get_records()
        timestamp = self.client.get_records_timestamp()
        assert timestamp == '12345'
        assert self.session.request.call_count == 1

    def test_records_timestamp_is_cached_per_collection(self):
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"12345"'})
        self.client.get_records(collection="foo")
        mock_response(self.session, data=[{'id': 'foo'}, {'id': 'bar'}],
                      headers={"ETag": '"67890"'})
        self.client.get_records(collection="bar")

        timestamp = self.client.get_records_timestamp(collection="foo")
        assert timestamp == '12345'

        timestamp = self.client.get_records_timestamp(collection="bar")
        assert timestamp == '67890'

    def test_pagination_is_followed(self):
        # Mock the calls to request.
        link = ('http://example.org/buckets/buck/collections/coll/records/'
                '?token=1234')

        self.session.request.side_effect = [
            # First one returns a list of items with a pagination token.
            build_response(
                [{'id': '1', 'value': 'item1'},
                 {'id': '2', 'value': 'item2'}, ],
                {'Next-Page': link}),
            build_response(
                [{'id': '3', 'value': 'item3'},
                 {'id': '4', 'value': 'item4'}, ],
                {'Next-Page': link}),
            # Second one returns a list of items without a pagination token.
            build_response(
                [{'id': '5', 'value': 'item5'},
                 {'id': '6', 'value': 'item6'}, ],
            ),
        ]
        records = self.client.get_records(bucket='bucket', collection='collection')

        assert list(records) == [
            {'id': '1', 'value': 'item1'},
            {'id': '2', 'value': 'item2'},
            {'id': '3', 'value': 'item3'},
            {'id': '4', 'value': 'item4'},
            {'id': '5', 'value': 'item5'},
            {'id': '6', 'value': 'item6'},
        ]

    def test_pagination_is_followed_for_number_of_pages(self):
        # Mock the calls to request.
        link = ('http://example.org/buckets/buck/collections/coll/records/'
                '?token=1234')

        self.session.request.side_effect = [
            # First one returns a list of items with a pagination token.
            build_response(
                [{'id': '1', 'value': 'item1'},
                 {'id': '2', 'value': 'item2'}, ],
                {'Next-Page': link}),
            build_response(
                [{'id': '3', 'value': 'item3'},
                 {'id': '4', 'value': 'item4'}, ],
                {'Next-Page': link}),
            # Second one returns a list of items without a pagination token.
            build_response(
                [{'id': '5', 'value': 'item5'},
                 {'id': '6', 'value': 'item6'}, ],
            ),
        ]
        records = self.client.get_records(bucket='bucket', collection='collection', pages=2)

        assert list(records) == [
            {'id': '1', 'value': 'item1'},
            {'id': '2', 'value': 'item2'},
            {'id': '3', 'value': 'item3'},
            {'id': '4', 'value': 'item4'},
        ]

    def test_pagination_is_not_followed_if_limit_is_specified(self):
        # Mock the calls to request.
        link = ('http://example.org/buckets/buck/collections/coll/records/'
                '?token=1234')

        self.session.request.side_effect = [
            build_response(
                [{'id': '1', 'value': 'item1'},
                 {'id': '2', 'value': 'item2'}, ],
                {'Next-Page': link}),
            build_response(
                [{'id': '3', 'value': 'item3'},
                 {'id': '4', 'value': 'item4'}, ],
            ),
        ]
        records = self.client.get_records(bucket='bucket', collection='collection', _limit=2)

        assert list(records) == [
            {'id': '1', 'value': 'item1'},
            {'id': '2', 'value': 'item2'}
        ]

    def test_pagination_supports_if_none_match(self):
        link = ('http://example.org/buckets/buck/collections/coll/records/'
                '?token=1234')

        self.session.request.side_effect = [
            # First one returns a list of items with a pagination token.
            build_response(
                [{'id': '1', 'value': 'item1'},
                 {'id': '2', 'value': 'item2'}, ],
                {'Next-Page': link}),
            # Second one returns a list of items without a pagination token.
            build_response(
                [{'id': '3', 'value': 'item3'},
                 {'id': '4', 'value': 'item4'}, ],
            ),
        ]
        self.client.get_records(bucket='bucket', collection='collection',
                                if_none_match="1234")

        # Check that the If-None-Match header is present in the requests.
        self.session.request.assert_any_call(
            'get', '/buckets/bucket/collections/collection/records',
            headers={'If-None-Match': '"1234"'}, params={})
        self.session.request.assert_any_call(
            'get', link, headers={'If-None-Match': '"1234"'}, params={})

    def test_collection_can_delete_a_record(self):
        mock_response(self.session, data={'id': 1234})
        resp = self.client.delete_record(id=1234)
        assert resp == {'id': 1234}
        url = '/buckets/mybucket/collections/mycollection/records/1234'
        self.session.request.assert_called_with('delete', url, headers=None)

    def test_record_delete_if_match(self):
        data = {}
        mock_response(self.session, data=data)
        deleted = self.client.delete_record(collection='mycollection',
                                            bucket='mybucket',
                                            id='1',
                                            if_match=1234)
        assert deleted == data
        url = '/buckets/mybucket/collections/mycollection/records/1'
        self.session.request.assert_called_with(
            'delete', url, headers={'If-Match': '"1234"'})

    def test_record_delete_if_match_not_included_if_not_safe(self):
        data = {}
        mock_response(self.session, data=data)
        deleted = self.client.delete_record(collection='mycollection',
                                            bucket='mybucket',
                                            id='1',
                                            if_match=1234,
                                            safe=False)
        assert deleted == data
        url = '/buckets/mybucket/collections/mycollection/records/1'
        self.session.request.assert_called_with(
            'delete', url, headers=None)

    def test_update_record_gets_the_id_from_data_if_exists(self):
        mock_response(self.session)
        self.client.update_record(bucket='mybucket', collection='mycollection',
                                  data={'id': 1, 'foo': 'bar'})

        self.session.request.assert_called_with(
            'put', '/buckets/mybucket/collections/mycollection/records/1',
            data={'id': 1, 'foo': 'bar'}, headers=None, permissions=None)

    def test_update_record_handles_if_match(self):
        mock_response(self.session)
        self.client.update_record(bucket='mybucket', collection='mycollection',
                                  data={'id': 1, 'foo': 'bar'}, if_match=1234)

        headers = {'If-Match': '"1234"'}
        self.session.request.assert_called_with(
            'put', '/buckets/mybucket/collections/mycollection/records/1',
            data={'id': 1, 'foo': 'bar'}, headers=headers, permissions=None)

    def test_patch_record_uses_the_patch_method(self):
        mock_response(self.session)
        self.client.patch_record(bucket='mybucket', collection='mycollection',
                                 data={'id': 1, 'foo': 'bar'})

        self.session.request.assert_called_with(
            'patch', '/buckets/mybucket/collections/mycollection/records/1',
            payload={'data': {'id': 1, 'foo': 'bar'}},
            headers={"Content-Type": "application/json"})

    def test_patch_record_recognizes_patchtype(self):
        mock_response(self.session)
        self.client.patch_record(bucket='mybucket', collection='mycollection',
                                 changes=MergePatch({'foo': 'bar'}, {'read': ['alice']}), id=1)

        self.session.request.assert_called_with(
            'patch', '/buckets/mybucket/collections/mycollection/records/1',
            payload={'data': {'foo': 'bar'}, 'permissions': {'read': ['alice']}},
            headers={"Content-Type": "application/merge-patch+json"},
        )

    def test_patch_record_understands_jsonpatch(self):
        mock_response(self.session)
        self.client.patch_record(
            bucket='mybucket', collection='mycollection',
            changes=JSONPatch([{'op': 'add', 'patch': '/baz', 'value': 'qux'}]), id=1)

        self.session.request.assert_called_with(
            'patch', '/buckets/mybucket/collections/mycollection/records/1',
            payload=[{'op': 'add', 'patch': '/baz', 'value': 'qux'}],
            headers={"Content-Type": "application/json-patch+json"},
        )

    def test_patch_record_requires_data_to_be_patch_type(self):
        with pytest.raises(TypeError, match="couldn't understand patch body 5"):
            self.client.patch_record(id=1, collection='testcoll', bucket='testbucket', changes=5)

    def test_patch_record_requires_id(self):
        with pytest.raises(KeyError, match="Unable to patch record, need an id."):
            self.client.patch_record(collection='testcoll', bucket='testbucket', data={})

    def test_update_record_raises_if_no_id_is_given(self):
        with self.assertRaises(KeyError) as cm:
            self.client.update_record(data={'foo': 'bar'},  # Omit the id on purpose here.
                                      bucket='mybucket',
                                      collection='mycollection')
        assert str(cm.exception) == "'Unable to update a record, need an id.'"

    def test_get_or_create_doesnt_raise_in_case_of_conflict(self):
        data = {
            'permissions': mock.sentinel.permissions,
            'data': {'foo': 'bar'}
        }
        self.session.request.side_effect = [
            get_http_error(status=412),
            (data, None)
        ]
        returned_data = self.client.create_record(bucket="buck",
                                                  collection="coll",
                                                  data={'id': 1234,
                                                        'foo': 'bar'},
                                                  if_not_exists=True)  # Should not raise.
        assert returned_data == data

    def test_get_or_create_raise_in_other_cases(self):
        self.session.request.side_effect = get_http_error(status=500)
        with self.assertRaises(KintoException):
            self.client.create_record(bucket="buck",
                                      collection="coll",
                                      data={'foo': 'bar'},
                                      id='record',
                                      if_not_exists=True)

    def test_create_record_raises_a_special_error_on_403(self):
        self.session.request.side_effect = get_http_error(status=403)
        with self.assertRaises(KintoException) as e:
            self.client.create_record(bucket="buck",
                                      collection="coll",
                                      data={'foo': 'bar'})
        expected_msg = ("Unauthorized. Please check that the collection exists"
                        " and that you have the permission to create or write "
                        "on this collection record.")
        assert e.exception.message == expected_msg

    def test_create_record_can_deduce_id_from_data(self):
        self.client.create_record(data={'id': 'record'}, bucket='buck', collection='coll')
        self.session.request.assert_called_with(
            'put', '/buckets/buck/collections/coll/records/record', data={'id': 'record'},
            permissions=None, headers=DO_NOT_OVERWRITE)

    def test_update_record_can_deduce_id_from_data(self):
        self.client.update_record(data={'id': 'record'}, bucket='buck', collection='coll')
        self.session.request.assert_called_with(
            'put', '/buckets/buck/collections/coll/records/record', data={'id': 'record'},
            permissions=None, headers=None)
예제 #11
0
class RecordLoggingTest(unittest.TestCase):
    def setUp(self):
        self.session = mock.MagicMock()
        self.client = Client(session=self.session)
        mock_response(self.session)

    def test_create_record_logs_info_message(self):
        with mock.patch("kinto_http.logger") as mocked_logger:
            self.client.create_bucket(id="buck")
            self.client.create_collection(id="mozilla", bucket="buck")
            self.client.create_record(
                id="fake-record",
                data={"foo": "bar"},
                permissions={"write": ["blah"]},
                bucket="buck",
                collection="mozilla",
            )
            mocked_logger.info.assert_called_with(
                "Create record with id 'fake-record' in collection 'mozilla' in bucket 'buck'"
            )

    def test_update_record_logs_info_message(self):
        with mock.patch("kinto_http.logger") as mocked_logger:
            self.client.create_bucket(id="buck")
            self.client.create_collection(bucket="buck", id="mozilla")
            self.client.update_record(id="fake-record",
                                      data={"ss": "aa"},
                                      bucket="buck",
                                      collection="mozilla")
            mocked_logger.info.assert_called_with(
                "Update record with id 'fake-record' in collection 'mozilla' in bucket 'buck'"
            )

    def test_patch_record_logs_info_message(self):
        with mock.patch("kinto_http.logger") as mocked_logger:
            self.client.create_bucket(id="buck")
            self.client.create_collection(bucket="buck", id="mozilla")
            self.client.patch_record(id="fake-record",
                                     data={"ss": "aa"},
                                     bucket="buck",
                                     collection="mozilla")
            mocked_logger.info.assert_called_with(
                "Patch record with id 'fake-record' in collection 'mozilla' in bucket 'buck'"
            )

    def test_get_record_logs_info_message(self):
        with mock.patch("kinto_http.logger") as mocked_logger:
            self.client.create_bucket(id="buck")
            self.client.create_collection(id="mozilla", bucket="buck")
            self.client.get_record(id="fake-record",
                                   bucket="buck",
                                   collection="mozilla")
            mocked_logger.info.assert_called_with(
                "Get record with id 'fake-record' from collection 'mozilla' in bucket 'buck'"
            )

    def test_delete_record_logs_info_message(self):
        with mock.patch("kinto_http.logger") as mocked_logger:
            self.client.create_bucket(id="buck")
            self.client.create_collection(id="mozilla", bucket="buck")
            self.client.delete_record(id="fake-record",
                                      bucket="buck",
                                      collection="mozilla")
            mocked_logger.info.assert_called_with(
                "Delete record with id 'fake-record' from collection 'mozilla' in bucket 'buck'"
            )

    def test_delete_records_logs_info_message(self):
        with mock.patch("kinto_http.logger") as mocked_logger:
            self.client.create_bucket(id="buck")
            self.client.create_collection(id="mozilla", bucket="buck")
            self.client.delete_records(bucket="buck", collection="mozilla")
            mocked_logger.info.assert_called_with(
                "Delete records from collection 'mozilla' in bucket 'buck'")
예제 #12
0
파일: main.py 프로젝트: njouanin/joliebulle
class AppWindow(QtWidgets.QMainWindow,Ui_MainWindow):
    def __init__(self, parent = None):
        QtWidgets.QMainWindow.__init__(self, parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setupUi(self)




######################################################################################
######################################################################################
        self.settings = Settings()
        self.initRep()
        self.initKintoClient()
        self.syncRecipes()
        self.dlgEditG = Dialog(self)
        self.dlgEditH = DialogH(self)
        self.dlgEditD = DialogD(self)
        self.dlgEditY = DialogL(self)
        self.dlgPref = DialogPref(self)
        self.dlgStep = DialogStep(self)
        self.dlgMash = DialogMash(self)



        self.base = ImportBase()
        self.mashProfileExport = ExportMash()


#        self.base.importBeerXML()
        self.s=0
        self.recipe = None


        #Les connexions
        self.actionEnregistrer.triggered.connect(self.enregistrer)
        self.actionQuitter.triggered.connect(app.quit)
#        self.connect(self.actionQuitter, QtCore.SIGNAL("triggered()"), app, QtCore.SLOT("quit()"))

        self.actionShowJournal.triggered.connect(self.showJournal)


        self.actionEditGrains.triggered.connect(self.editGrains)
        self.actionEditHoublons.triggered.connect(self.editHoublons)
        self.actionEditDivers.triggered.connect(self.editDivers)
        self.actionEditLevures.triggered.connect(self.editLevures)
        self.actionRestaurerIngredients.triggered.connect(self.restoreDataBase)
        self.actionImportIng.triggered.connect(self.importIng)
        self.actionManageProfiles.triggered.connect(self.seeMash)

        self.actionAbout.triggered.connect(self.about)

        self.actionAllTools.triggered.connect(self.showTools)

        self.actionPreferences.triggered.connect(self.dialogPreferences)



        #######################################################################################################
        # Profil de brassage       #########################################################################################################


        self.listWidgetSteps.itemSelectionChanged.connect (self.stepDetails)
        self.listWidgetMashProfiles.itemSelectionChanged.connect (self.mashClicked)
        self.buttonBoxMashDetails.rejected.connect(self.mashRejected)
#        self.comboBoxStepType.addItems(["Infusion", "Température", "Décoction"])
        self.pushButtonStepEdit.clicked.connect(self.stepEdit)
        self.dlgStep.stepChanged.connect(self.stepReload)
        self.pushButtonStepRemove.clicked.connect(self.removeStep)
        self.pushButtonNewStep.clicked.connect(self.addStep)
        self.pushButtonMashEdit.clicked.connect(self.mashEdit)
        self.dlgMash.mashChanged.connect(self.mashReload)
        self.pushButtonNewProfile.clicked.connect(self.addMash)
        self.pushButtonRemoveProfile.clicked.connect(self.removeMash)
        self.pushButtonSaveProfile.clicked.connect(self.saveProfile)

        #La bibliotheque
        ###################################################################################################################
        ###################################################################################################################


        # self.listdir(recettes_dir)
        self.showLib()

###################################################################################################
######## gestion des arguments au lancement du programme  #########################################


        argumentsList=QtWidgets.QApplication.arguments()
        if len(argumentsList) > 1 :
            logger.debug("la liste d'arguments: %s",argumentsList)
            logger.debug("le chemin: %s",argumentsList[1])
            # for part in argumentsList :
            #     recipePath=recipePath + " " + part
            try:
                recipePath= argumentsList[1]
                for part in argumentsList[2:] :
                    recipePath= recipePath +" " + part

                self.openRecipeFile(recipePath)
            except :
                pass
        else:
            pass

########################################################################################################################
####################################################################################################################
# le signal émit à la fermeture de la fenêtre de préférences
        self.dlgPref.prefAccepted.connect(self.prefReload)



###########################################################
############### Journal ##############################
######################################################

    def loadJournal(self):
        self.journal=Journal()
        self.journal.loadJournal()
        # self.actionEditJournal.setEnabled(True)

    @QtCore.pyqtSlot()
    def showJournal(self,entry=" '' ") :
        self.stackedWidget.setCurrentIndex(0)
        self.loadJournal()
        pyDir = os.path.abspath(os.path.dirname(__file__))
        baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/"))
        self.webViewBiblio.setHtml(self.journal.export("html",entry), baseUrl)
        self.webViewBiblio.page().mainFrame().addToJavaScriptWindowObject("main", self)
        # self.webViewBiblio.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
        # self.webInspector = QtWebKit.QWebInspector(self)
        # self.webInspector.setPage(self.webViewBiblio.page())
        # self.webInspector.setVisible(True)
        # self.verticalLayout_13.addWidget(self.webInspector)


    @QtCore.pyqtSlot(str, str)
    def addToJournal(self,event, recipeName) :
        self.loadJournal()
        entry = '''{recipe:%s,date:%s,event:%s,editing:'True'} ''' %( "'" + recipeName + "'", "'" + str(int(time.time())) + "'" , "'" + self.journal.eventsLabels[event] + "'")
        self.showJournal(entry)


    @QtCore.pyqtSlot(str)
    def dumpJournal(self,journalJson) :
        journalJson= '{"name":"journal","items": %s }' %journalJson
        d=json.loads(journalJson)
        with open(journal_file, mode="w", encoding="utf-8") as f :
            json.dump(d,f,indent=2)



############## Bibliothèque ##############################
##########################################################
    @QtCore.pyqtSlot()
    def showLib(self) :
        # data = json.dumps(self.recipesSummary)
        # data = data.replace("'","'")
        self.stackedWidget.setCurrentIndex(0)
        self.brewdayLock = 0

        self.webSettings = self.webViewBiblio.settings()
        self.webSettings.setAttribute(QtWebKit.QWebSettings.LocalContentCanAccessRemoteUrls, True)

        pyDir = os.path.abspath(os.path.dirname(__file__))
        baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/html/"))
        self.webViewBiblio.setHtml(LibExporterRepository['html'](), baseUrl)
        self.webViewBiblio.page().mainFrame().addToJavaScriptWindowObject("main", self)
        self.webViewBiblio.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
        self.webViewBiblio.page().action(QtWebKitWidgets.QWebPage.Reload).setVisible(False)


    @QtCore.pyqtSlot(str)
    def deleteLib(self,path) :
        confirmation = QtWidgets.QMessageBox.question(self,
                            self.tr("Supprimer"),
                            self.tr("La recette sera définitivement supprimée <br/> Continuer ?"),
                            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        if (confirmation == QtWidgets.QMessageBox.Yes):
            os.remove(path)
            self.listdir(recettes_dir)
            self.showLib()
        else :
            self.showLib()


    @QtCore.pyqtSlot()
    def backWebViewBiblio(self) :
        self.stackedWidget.setCurrentIndex(0)


    @QtCore.pyqtSlot(str, str)
    def saveRecipe(self, recipe, path) :
        logger.debug(path)
        recipeFile = QtCore.QFile(path)
        if recipeFile.open(QtCore.QIODevice.WriteOnly):
            try:
                stream = QtCore.QTextStream(recipeFile)
                stream.setCodec("UTF-8")
                stream << recipe
            finally:
                recipeFile.close()
        else:
            # TODO : Prévenir l'utilisateur en cas d'échec de l'enregistrement
            pass

    @QtCore.pyqtSlot(result=str)
    def createPath(self, file_id=None) :
        if file_id is None:
            file_id = str(int(time.time()*10))
        path = recettes_dir + "/" + file_id + ".xml"
        logger.debug(path)
        return path

    @QtCore.pyqtSlot()
    def resetLock(self):
        self.brewdayLock = 0;




############# Mode Brassage ################################
############################################################

    @QtCore.pyqtSlot(str)
    def showBrewdayMode(self, data):
        if self.brewdayLock == 0 :
            self.stackedWidget.setCurrentIndex(1)
            self.brewdayLock = 1
            data = data.replace("'","&#39;")
            pyDir = os.path.abspath(os.path.dirname(__file__))
            baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/"))
            self.webViewBrewday.setHtml(BrewdayExporterRepository['html'](data), baseUrl)
            self.webViewBrewday.page().mainFrame().addToJavaScriptWindowObject("main", self)
            self.webViewBrewday.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
            self.webViewBrewday.page().action(QtWebKitWidgets.QWebPage.Reload).setVisible(False)
        else :
            self.stackedWidget.setCurrentIndex(1)





###### Outils ############################################
##########################################################

    @QtCore.pyqtSlot()
    def showTools(self):
        self.stackedWidget.setCurrentIndex(0)
        pyDir = os.path.abspath(os.path.dirname(__file__))
        baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/"))
        self.webViewBiblio.setHtml(ToolExporterRepository["html"](), baseUrl)
        self.webViewBiblio.page().mainFrame().addToJavaScriptWindowObject("main", self)
        # self.webViewBiblio.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
        # self.webInspector = QtWebKit.QWebInspector(self)
        # self.webInspector.setPage(self.webViewBiblio.page())
        # self.webInspector.setVisible(True)
        # self.verticalLayout_13.addWidget(self.webInspector)



    @QtCore.pyqtSlot(result=str)
    def dataRecipes(self) :
        # f = open(recipeData_file, 'w')
        # f.write(self.recipesSummary)
        self.listdir(recettes_dir)
        return self.recipesSummary

    @QtCore.pyqtSlot(result=str)
    def dataProfiles(self) :
        return self.mashProfileExport.exportJson(ImportBase().listeMashes)


    @QtCore.pyqtSlot(result=str)
    def dataIngredients(self) :
        return ImportBase().exportjson()


    @QtCore.pyqtSlot(result=str)
    def dataPref(self) :
        dic = {}
        dic["boilOffRate"] = settings.conf.value("BoilOffRate")
        dic["coolingLossRate"] = settings.conf.value("CoolingLoss")
        dic["grainTemp"] = settings.conf.value("GrainTemp")
        dic["fudgeFactor"] = settings.conf.value("FudgeFactor")
        dic["grainRetention"] = settings.conf.value("GrainRetention")
        dic = json.dumps(dic)
        return dic











    #Une fonction qui gère l'aperçu des couleurs.
    #Contient un tupple avec plusieurs références de couleurs, classées par rang selon la valeur SRM.
    #################################################################################################
    # def colorPreview (self) :
    #     self.colorTuppleSrm = ('FFE699', 'FFD878', 'FFCA5A', 'FFBF42', 'FBB123', 'F8A600', 'F39C00', 'EA8F00', 'E58500', 'DE7C00', 'D77200', 'CF6900', 'CB6200', 'C35900','BB5100', 'B54C00', 'B04500', 'A63E00', 'A13700', '9B3200', '952D00', '8E2900', '882300', '821E00', '7B1A00', '771900', '701400', '6A0E00', '660D00','5E0B00','5A0A02','600903', '520907', '4C0505', '470606', '440607', '3F0708', '3B0607', '3A070B', '36080A')

    #     colorRef= round(self.recipe.compute_EBC()/1.97)

    #     if colorRef >= 30 :
    #         color = "#" + self.colorTuppleSrm[30]
    #     elif colorRef <= 1 :
    #         color = "#" + self.colorTuppleSrm[0]
    #     else :
    #         color = "#" + self.colorTuppleSrm[colorRef-1]
    #     self.widgetColor.setStyleSheet("background-color :" + color)


    def liste_fichiers_recettes(self, rootdir):
        for root, subFolders, files in os.walk(rootdir):
            for file2 in files:
                yield (file2, os.path.join(root,file2))

    def listdir(self, rootdir) :
        summaries=[]
        for filename, recipe in self.liste_fichiers_recettes(rootdir):
            try :
                summaries.append(self.jsonRecipeLib(recipe))
            except :
                logger.debug("le fichier %s n'est pas une recette" %(recipe))
        self.recipesSummary = "[" + ",".join(summaries) + "]"
        logger.debug("%s recettes détectées" %(len(summaries)))



    def jsonRecipeLib(self,recipe) :
        self.s = recipe
        self.recipe = Recipe.parse(recipe)
        data = self.recipe.export("json")
        data = data[1:-1]
        return data



    def initRep(self) :
        home = QtCore.QDir(home_dir)
        config = QtCore.QDir(config_dir)
        logger.debug (config)
        if not config.exists() :
            home.mkpath (config_dir)
        else :
            pass
        database = QtCore.QFile(database_file)
        if not database.exists() :
            database.copy(database_root, database_file)
        else :
            pass
        recettes = QtCore.QFile(recettes_dir)
        if not recettes.exists() :
            try :
                shutil.copytree(samples_dir, samples_target)
            except :
                home.mkpath(recettes_dir)
        mash  = QtCore.QFile(mash_file)
        if not mash.exists() :
            mash.copy(mash_root, mash_file)
        else :
            pass
        journal  = QtCore.QFile(journal_file)
        if not journal.exists() :
            journal.copy(journal_root, journal_file)
        else :
            pass

        # on configure des valeurs par défaut
        if not settings.conf.contains("BoilOffRate") :
            settings.conf.setValue("BoilOffRate", 10)
        if not settings.conf.contains("CoolingLoss") :
            settings.conf.setValue("CoolingLoss", 5)
        if not settings.conf.contains("GrainTemp") :
            settings.conf.setValue("GrainTemp", 20)
        if not settings.conf.contains("FudgeFactor") :
            settings.conf.setValue("FudgeFactor", 1.7)
        if not settings.conf.contains("GrainRetention") :
            settings.conf.setValue("GrainRetention", 1)
        if not settings.conf.contains("Menus") :
            settings.conf.setValue("Menus", "button")


    def prefReload(self) :
        if platform == 'win32':
            recettes_dir = settings.conf.value("pathWin32")
        else :
            recettes_dir = settings.conf.value("pathUnix")
        self.initRep()
        self.initKintoClient()
        self.listdir(recettes_dir)
        self.showLib()

    def initKintoClient(self):
        self.kinto_client = None
        kinto_url = settings.conf.value("KintoServerUrl")
        if kinto_url is not None and kinto_url != "":
            kinto_bucket = settings.conf.value("KintoDefaultBucket")
            kinto_credentials=(settings.conf.value("KintoBasicUserCred"), settings.conf.value("KintoBasicPasswordCred"))
            logger.debug(kinto_credentials)
            if kinto_bucket is None or kinto_bucket == "":
                kinto_bucket = "joliebulle"
                logger.debug("using default bucket 'joliebulle'")
            try:
                tmp_client = Client(server_url=kinto_url, auth = kinto_credentials)
                tmp_client.create_bucket(id=kinto_bucket, if_not_exists=True)
                self.kinto_client = Client(server_url=kinto_url, bucket=kinto_bucket, auth = kinto_credentials)

                # Création des collections
                self.kinto_client.create_collection(id='recipes', if_not_exists=True)
                self.kinto_client.create_collection(id='ingredients', if_not_exists=True)
                
                logger.info("Synchronize with kinto server at :" + repr(self.kinto_client))
            except Exception as e:
                logger.warn("Failed to initialize Kinto synchronisation: " + repr(e))

    def syncRecipes(self):
        recipes_collection = 'recipes'
        if self.kinto_client is None:
            return

        # Sync local recipes
        id_recettes_locales=[]
        for filename, full_filename in self.liste_fichiers_recettes(recettes_dir):
            recipe_id = filename.replace('.xml', '')
            id_recettes_locales.append(recipe_id)
            try:
                remote_recipe = self.kinto_client.get_record(id=recipe_id, collection=recipes_collection)
                logger.debug("recipe " + recipe_id + " already exists on server, update if needed")
                remote_timestamp = int(remote_recipe['data']['last_modified']/1000)
                local_timestamp = int(os.path.getmtime(full_filename))
                logger.debug(str(remote_timestamp) + " " + str(local_timestamp))
                if remote_timestamp < local_timestamp:
                    logger.info("Mise à jour de la recette distante " + recipe_id)
                    # La recette distant doit être mise à jour
                    local_recipe = Recipe.parse(full_filename)
                    data = local_recipe.export("dict")
                    ret = self.kinto_client.update_record(id=recipe_id, collection=recipes_collection, data=data)
                    new_timestamp = ret['data']['last_modified']
                    #MAJ du mtime du fichier pour marquer la synchronisation
                    os.utime(full_filename, times=(int(new_timestamp/1000), int(new_timestamp/1000)))
                if remote_timestamp > local_timestamp:
                    # La recette distante doit être mise à jour
                    logger.info("Mise à jour de la recette locale " + recipe_id)
                    new_recipe = Recipe.parse(remote_recipe['data'], "dict")
                    self.doEnregistrerRecette(new_recipe, full_filename)
                    os.utime(full_filename, times=(int(remote_timestamp), int(remote_timestamp)))
            except KintoException:
                logger.debug("recipe " + recipe_id + " doesn't exists on server, sending it")
                new_recipe = Recipe.parse(full_filename)
                data = new_recipe.export("dict")
                ret = self.kinto_client.create_record(id=recipe_id, collection=recipes_collection, data=data)
                new_timestamp = ret['data']['last_modified']
                #MAJ du mtime du fichier pour marquer la synchronisation
                os.utime(full_filename, times=(int(new_timestamp/1000), int(new_timestamp/1000)))

        recipes = self.kinto_client.get_records(collection='recipes')
        for recipe in recipes:
            if recipe['id'] not in id_recettes_locales:
                #Recette distante non présente localement
                new_recipe = Recipe.parse(recipe, "dict")
                fullname = recipe['id'] + ".xml"
                logger.debug("Création de la recette " + fullname)
                self.doEnregistrerRecette(new_recipe, os.path.join(recettes_dir, fullname))
                os.utime(full_filename, times=(int(remote_timestamp), int(remote_timestamp)))


    @QtCore.pyqtSlot()
    def switchToLibrary(self) :
        self.stackedWidget.setCurrentIndex(0)
        # self.viewRecipeLib(self.s)

    def switchToMash(self) :
        self.stackedWidget.setCurrentIndex(2)


    def restoreDataBase(self) :
        home = QtCore.QDir(home_dir)
        config = QtCore.QDir(config_dir)
        database = QtCore.QFile(database_file)
        confirmation = QtWidgets.QMessageBox.question(self,
                                    self.tr("Remplacer la base ?"),
                                    self.tr("La base des ingrédients actuelle va être effacée et remplacée par la base originale. Toutes vos modifications vont être effacées. Un redémarrage de l'application sera nécessaire.<br> Continuer ?"),
                                    QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        if (confirmation == QtWidgets.QMessageBox.Yes):
            database.remove(database_file)
            database.copy(database_root, database_file)
        else :

            pass


    def editGrains(self) :
        self.dlgEditG.setModal(True)
        self.dlgEditG.setModel()
        self.dlgEditG.show()

    def editHoublons(self) :
        self.dlgEditH.setModal(True)
        self.dlgEditH.setModel()
        self.dlgEditH.show()

    def editDivers(self) :
        self.dlgEditD.setModal(True)
        self.dlgEditD.setModel()
        self.dlgEditD.show()

    def editLevures(self) :
        self.dlgEditY.setModal(True)
        self.dlgEditY.setModel()
        self.dlgEditY.show()

    @QtCore.pyqtSlot(float, float, float, float)
    def preBoilCheck(self,volPreBoil,preBoilSg,GU,volume) :
        self.dlgPreBoil = DialogPreBoil(self)
        self.dlgPreBoil.setData(volPreBoil,preBoilSg,GU,volume)
        self.dlgPreBoil.setModal(True)
        self.dlgPreBoil.show()


    def dialogPreferences (self) :
        self.dlgPref.setModal(True)
        self.dlgPref.show()




    def importBeerXML(self) :
        fichierBeerXML = self.s
        try:
            self.recipe = Recipe.parse(fichierBeerXML)
            self.currentRecipeMash = self.recipe.mash

        except :
            errors = Errors()
            errors.warningXml()


    def about(self) :
        about = DialogAbout(self)
        about.show()


    def doEnregistrerRecette(self, recipe, destination):
        recipeFile = QtCore.QFile(destination)
        if recipeFile.open(QtCore.QIODevice.WriteOnly):
            try:
                stream = QtCore.QTextStream(recipeFile)
                stream.setCodec("UTF-8")
                stream << recipe.export("beerxml")
            finally:
                recipeFile.close()
        else:
            # TODO : Prévenir l'utilisateur en cas d'échec de l'enregistrement
            pass

    def enregistrerRecette(self, destination):
        self.doEnregistrerRecette(self.recipe, destination)
        self.fileSaved = True

    def enregistrer (self) :
        if self.recipe.name != self.lineEditRecette.text() :
            self.nameChanged = True
        else :
            self.nameChanged = False

        self.recipe.name = self.lineEditRecette.text()
        self.recipe.style = self.lineEditGenre.text()
        self.recipe.brewer = self.lineEditBrewer.text()
        self.recipe.boil = self.spinBoxBoil.value()
        if not self.s:
            destination = recettes_dir + "/" + self.recipe.name.replace('/', ' ') + ".xml"
            if os.path.exists(destination) :
                errors=Errors()
                errors.warningExistingPath()
                self.fileSaved = False
            else :
                self.s = destination
                self.enregistrerRecette(destination)
        else :
            self.enregistrerRecette(self.s)




    def enregistrerSous (self) :
        self.s = QtGui.QFileDialog.getSaveFileName (self,
                                                    self.tr("Enregistrer dans un fichier"),
                                                    recettes_dir + "/" + self.recipe.name.replace('/', ' ') + ".xml",
                                                    "BeerXML (*.xml)")
        self.enregistrerRecette(self.s)

    @QtCore.pyqtSlot(str)
    def copyBbcode (self, bbcode):
        app.clipboard().setText(bbcode)


    def importIng(self):
        s = QtWidgets.QFileDialog.getOpenFileName(self,
            self.tr("Ouvrir un fichier"),
            home_dir,
            )
        if not s :
            pass
        else :
            self.importIngList = ImportIng()
            self.importIngList.parseFile(s)


    def mashComboChanged (self) :
        #on remet le verrou à 0, il va falloir recalculer en repassant en brewday mode
        self.brewdayLock = 0
        try :
            i =self.comboBoxMashProfiles.currentIndex()
            self.currentMash = ImportBase().listeMashes[i]
        except :
            self.currentMash = self.currentRecipeMash
        if i == -1 :
            self.currentMash = Mash()
        self.recipe.mash = self.currentMash

    def seeMash(self) :
        self.switchToMash()
        index = self.listWidgetMashProfiles.currentRow()
        i = self.listWidgetSteps.currentRow()
        self.listWidgetMashProfiles.clear()
        self.listWidgetSteps.clear()

        self.numMash = len(ImportBase().listeMashes)
        #self.numSteps = self.mashProfilesBase.numSteps
        self.popMashList()
        self.pushButtonMashEdit.setEnabled(False)
        self.pushButtonRemoveProfile.setEnabled(False)
        self.pushButtonStepRemove.setEnabled(False)
        self.pushButtonStepEdit.setEnabled(False)
        self.listWidgetMashProfiles.setCurrentRow(index)
        self.listWidgetSteps.setCurrentRow(i)

    def popMashList(self) :
        self.listWidgetMashProfiles.clear()
        for mash in ImportBase().listeMashes :
           self.listWidgetMashProfiles.addItem(mash.name)

    def mashClicked(self) :
        self.listWidgetSteps.clear()
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            mash = ImportBase().listeMashes[index]
            for step in mash.listeSteps :
                self.listWidgetSteps.addItem(step.name)

            self.labelStepName.setTextFormat(QtCore.Qt.RichText)
            self.labelMashName.setText("<b>" + mash.name + "</b>")
            self.labelMashPh.setText("%.1f" %float(mash.ph))
    #        self.labelMashGrainTemp.setText("%.1f" %float(self.dicMashDetail['grainTemp']))
    #        self.labelMashTunTemp.setText("%.1f" %float(self.dicMashDetail['tunTemp']))
            try :
                self.labelMashSpargeTemp.setText("%.1f" %float(mash.spargeTemp))
            except :
                pass
            try :
                self.listWidgetSteps.setCurrentRow(0)
            except :
                pass
    #        print(self.dicMashDetail)
            self.pushButtonMashEdit.setEnabled(True)
            self.pushButtonRemoveProfile.setEnabled(True)

    def mashDetails(self) :
        self.dlgMashDetail = DialogMashDetail(self)
        self.dlgMashDetail.setModal(True)
        self.dlgMashDetail.show()
        self.dlgMashDetail.setFields(self.currentMash)
        self.dlgMashDetail.setAttribute( QtCore.Qt.WA_DeleteOnClose, True )


    def stepDetails(self) :
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                try:
                    selected_step = selected_mash.listeSteps[i]
                    self.labelStepName.setTextFormat(QtCore.Qt.RichText)
                    self.labelStepName.setText("<b>" + selected_step.name +"</b>")
                    self.labelStepType.setText(selected_step.type)
                    self.labelStepTemp.setText(MashStepView.temp_to_display(selected_step.temp))
                    self.labelStepTime.setText(MashStepView.time_to_display(selected_step.time))
                    self.pushButtonStepRemove.setEnabled(True)
                    self.pushButtonStepEdit.setEnabled(True)
                except:
                    pass


    def stepEdit(self) :
        index = self.listWidgetMashProfiles.currentRow()
        if  index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                selected_step = selected_mash.listeSteps[i]

                self.dlgStep.show()
                self.dlgStep.fields (selected_step)

    def stepReload(self, step) :
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                selected_step = selected_mash.listeSteps[i]

                selected_step.name = step.name
                selected_step.type = step.type
                selected_step.temp = step.temp
                selected_step.time = step.time
                self.seeMash()
                self.stepDetails()
                self.listWidgetMashProfiles.setCurrentRow(index)
                self.listWidgetSteps.setCurrentRow(i)

    def removeStep(self) :
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                item = self.listWidgetSteps.currentItem()
                del selected_mash.listeSteps[i]
                # self.listWidgetSteps.clearSelection()
                #self.listWidgetSteps.takeItem(item)
                #On force la sélection sur la ligne précédente
                self.listWidgetSteps.setCurrentRow(i-1)
                self.seeMash()

    def addStep(self) :
        index = self.listWidgetMashProfiles.currentRow()
        selected_mash = ImportBase().listeMashes[index]
        i = self.listWidgetSteps.currentRow()
        step = MashStep()
        step.name = 'Nouveau palier'
        step.type = 'Infusion'
        step.time = '0'
        step.temp = '0'
        step.vol = '0'
        selected_mash.listeSteps.append(step)

        self.listWidgetMashProfiles.setCurrentRow(index)
        self.seeMash()
        self.stepDetails()
        self.listWidgetMashProfiles.setCurrentRow(index)
        # self.listWidgetSteps.setCurrentRow(i-1)
        # self.stepEdit()

    def mashEdit(self) :
        index = self.listWidgetMashProfiles.currentRow()
        selected_mash = ImportBase().listeMashes[index]
        self.dlgMash.show()
        self.dlgMash.fields(selected_mash)

    def mashReload(self,mash) :
        #on remet le verrou à 0, il va falloir recalculer en repassant en brewday mode
        self.brewdayLock = 0
        f = self.listWidgetMashProfiles.currentRow()
        selected_mash = ImportBase().listeMashes[f]
        selected_mash.name = mash.name
        selected_mash.ph = mash.ph
        selected_mash.grainTemp = 20
        selected_mash.tunTemp = 20
        selected_mash.spargeTemp = mash.spargeTemp
        self.popMashList()
        self.listWidgetMashProfiles.setCurrentRow(f)

    def addMash(self) :
        new_mash = Mash()
        new_mash.name = 'Nouveau profil'
        new_mash.grainTemp = '0'
        new_mash.tunTemp = '0'
        new_mash.spargeTemp = '78'
        new_mash.ph = 5.4
        new_step = MashStep()
        new_step.name = 'Nouveau Palier'
        new_step.type = 'Infusion'
        new_step.time = '0'
        new_step.temp = '0'
        new_mash.listeSteps.append(new_step)
        ImportBase().listeMashes.append(new_mash)
        self.seeMash()
        self.listWidgetMashProfiles.setCurrentRow(len(ImportBase().listeMashes)-1)

    def removeMash(self) :
        i = self.listWidgetMashProfiles.currentRow()
        del ImportBase().listeMashes[i]
        self.seeMash()
        self.listWidgetSteps.clear()

    def mashRejected (self) :
        self.showLib()

    def saveProfile(self) :
        self.mashProfileExport.export(ImportBase().listeMashes)
        self.mashProfileExport.enregistrer(mash_file)

    @QtCore.pyqtSlot()
    def printRecipe (self) :
        printer=QtPrintSupport.QPrinter()
        dialog = QtPrintSupport.QPrintDialog(printer)
        dialog.setModal(True)
        dialog.setWindowTitle("Print Document" )
        if dialog.exec_() == True:
            self.webViewBiblio.print(printer)
            # document=QtGui.QTextDocument()
            # stringHtml=self.recipe.export("print")
            # document.setHtml(stringHtml)
            # document.print(printer)


    @QtCore.pyqtSlot()
    def printBrewday(self) :
        printer=QtPrintSupport.QPrinter()
        dialog = QtPrintSupport.QPrintDialog(printer)
        dialog.setModal(True)
        dialog.setWindowTitle("Print Document" )
        if dialog.exec_() == True:
            self.webViewBrewday.print(printer)
예제 #13
0
class AppWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        QtWidgets.QMainWindow.__init__(self, parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setupUi(self)

        ######################################################################################
        ######################################################################################
        self.settings = Settings()
        self.initRep()
        self.initKintoClient()
        self.syncRecipes()
        self.dlgEditG = Dialog(self)
        self.dlgEditH = DialogH(self)
        self.dlgEditD = DialogD(self)
        self.dlgEditY = DialogL(self)
        self.dlgPref = DialogPref(self)
        self.dlgStep = DialogStep(self)
        self.dlgMash = DialogMash(self)

        self.base = ImportBase()
        self.mashProfileExport = ExportMash()

        #        self.base.importBeerXML()
        self.s = 0
        self.recipe = None

        #Les connexions
        self.actionEnregistrer.triggered.connect(self.enregistrer)
        self.actionQuitter.triggered.connect(app.quit)
        #        self.connect(self.actionQuitter, QtCore.SIGNAL("triggered()"), app, QtCore.SLOT("quit()"))

        self.actionShowJournal.triggered.connect(self.showJournal)

        self.actionEditGrains.triggered.connect(self.editGrains)
        self.actionEditHoublons.triggered.connect(self.editHoublons)
        self.actionEditDivers.triggered.connect(self.editDivers)
        self.actionEditLevures.triggered.connect(self.editLevures)
        self.actionRestaurerIngredients.triggered.connect(self.restoreDataBase)
        self.actionImportIng.triggered.connect(self.importIng)
        self.actionManageProfiles.triggered.connect(self.seeMash)

        self.actionAbout.triggered.connect(self.about)

        self.actionAllTools.triggered.connect(self.showTools)

        self.actionPreferences.triggered.connect(self.dialogPreferences)

        #######################################################################################################
        # Profil de brassage       #########################################################################################################

        self.listWidgetSteps.itemSelectionChanged.connect(self.stepDetails)
        self.listWidgetMashProfiles.itemSelectionChanged.connect(
            self.mashClicked)
        self.buttonBoxMashDetails.rejected.connect(self.mashRejected)
        #        self.comboBoxStepType.addItems(["Infusion", "Température", "Décoction"])
        self.pushButtonStepEdit.clicked.connect(self.stepEdit)
        self.dlgStep.stepChanged.connect(self.stepReload)
        self.pushButtonStepRemove.clicked.connect(self.removeStep)
        self.pushButtonNewStep.clicked.connect(self.addStep)
        self.pushButtonMashEdit.clicked.connect(self.mashEdit)
        self.dlgMash.mashChanged.connect(self.mashReload)
        self.pushButtonNewProfile.clicked.connect(self.addMash)
        self.pushButtonRemoveProfile.clicked.connect(self.removeMash)
        self.pushButtonSaveProfile.clicked.connect(self.saveProfile)

        #La bibliotheque
        ###################################################################################################################
        ###################################################################################################################

        # self.listdir(recettes_dir)
        self.showLib()

        ###################################################################################################
        ######## gestion des arguments au lancement du programme  #########################################

        argumentsList = QtWidgets.QApplication.arguments()
        if len(argumentsList) > 1:
            logger.debug("la liste d'arguments: %s", argumentsList)
            logger.debug("le chemin: %s", argumentsList[1])
            # for part in argumentsList :
            #     recipePath=recipePath + " " + part
            try:
                recipePath = argumentsList[1]
                for part in argumentsList[2:]:
                    recipePath = recipePath + " " + part

                self.openRecipeFile(recipePath)
            except:
                pass
        else:
            pass

########################################################################################################################
####################################################################################################################
# le signal émit à la fermeture de la fenêtre de préférences
        self.dlgPref.prefAccepted.connect(self.prefReload)

###########################################################
############### Journal ##############################
######################################################

    def loadJournal(self):
        self.journal = Journal()
        self.journal.loadJournal()
        # self.actionEditJournal.setEnabled(True)

    @QtCore.pyqtSlot()
    def showJournal(self, entry=" '' "):
        self.stackedWidget.setCurrentIndex(0)
        self.loadJournal()
        pyDir = os.path.abspath(os.path.dirname(__file__))
        baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/"))
        self.webViewBiblio.setHtml(self.journal.export("html", entry), baseUrl)
        self.webViewBiblio.page().mainFrame().addToJavaScriptWindowObject(
            "main", self)
        # self.webViewBiblio.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
        # self.webInspector = QtWebKit.QWebInspector(self)
        # self.webInspector.setPage(self.webViewBiblio.page())
        # self.webInspector.setVisible(True)
        # self.verticalLayout_13.addWidget(self.webInspector)

    @QtCore.pyqtSlot(str, str)
    def addToJournal(self, event, recipeName):
        self.loadJournal()
        entry = '''{recipe:%s,date:%s,event:%s,editing:'True'} ''' % (
            "'" + recipeName + "'", "'" + str(int(time.time())) + "'",
            "'" + self.journal.eventsLabels[event] + "'")
        self.showJournal(entry)

    @QtCore.pyqtSlot(str)
    def dumpJournal(self, journalJson):
        journalJson = '{"name":"journal","items": %s }' % journalJson
        d = json.loads(journalJson)
        with open(journal_file, mode="w", encoding="utf-8") as f:
            json.dump(d, f, indent=2)

############## Bibliothèque ##############################
##########################################################

    @QtCore.pyqtSlot()
    def showLib(self):
        # data = json.dumps(self.recipesSummary)
        # data = data.replace("'","&#39;")
        self.stackedWidget.setCurrentIndex(0)
        self.brewdayLock = 0

        self.webSettings = self.webViewBiblio.settings()
        self.webSettings.setAttribute(
            QtWebKit.QWebSettings.LocalContentCanAccessRemoteUrls, True)

        pyDir = os.path.abspath(os.path.dirname(__file__))
        baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(
            pyDir, "static/html/"))
        self.webViewBiblio.setHtml(LibExporterRepository['html'](), baseUrl)
        self.webViewBiblio.page().mainFrame().addToJavaScriptWindowObject(
            "main", self)
        self.webViewBiblio.page().settings().setAttribute(
            QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
        self.webViewBiblio.page().action(
            QtWebKitWidgets.QWebPage.Reload).setVisible(False)

    @QtCore.pyqtSlot(str)
    def deleteLib(self, path):
        confirmation = QtWidgets.QMessageBox.question(
            self, self.tr("Supprimer"),
            self.tr(
                "La recette sera définitivement supprimée <br/> Continuer ?"),
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        if (confirmation == QtWidgets.QMessageBox.Yes):
            os.remove(path)
            self.listdir(recettes_dir)
            self.showLib()
        else:
            self.showLib()

    @QtCore.pyqtSlot()
    def backWebViewBiblio(self):
        self.stackedWidget.setCurrentIndex(0)

    @QtCore.pyqtSlot(str, str)
    def saveRecipe(self, recipe, path):
        logger.debug(path)
        recipeFile = QtCore.QFile(path)
        if recipeFile.open(QtCore.QIODevice.WriteOnly):
            try:
                stream = QtCore.QTextStream(recipeFile)
                stream.setCodec("UTF-8")
                stream << recipe
            finally:
                recipeFile.close()
        else:
            # TODO : Prévenir l'utilisateur en cas d'échec de l'enregistrement
            pass

    @QtCore.pyqtSlot(result=str)
    def createPath(self, file_id=None):
        if file_id is None:
            file_id = str(int(time.time() * 10))
        path = recettes_dir + "/" + file_id + ".xml"
        logger.debug(path)
        return path

    @QtCore.pyqtSlot()
    def resetLock(self):
        self.brewdayLock = 0

############# Mode Brassage ################################
############################################################

    @QtCore.pyqtSlot(str)
    def showBrewdayMode(self, data):
        if self.brewdayLock == 0:
            self.stackedWidget.setCurrentIndex(1)
            self.brewdayLock = 1
            data = data.replace("'", "&#39;")
            pyDir = os.path.abspath(os.path.dirname(__file__))
            baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/"))
            self.webViewBrewday.setHtml(
                BrewdayExporterRepository['html'](data), baseUrl)
            self.webViewBrewday.page().mainFrame().addToJavaScriptWindowObject(
                "main", self)
            self.webViewBrewday.page().settings().setAttribute(
                QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
            self.webViewBrewday.page().action(
                QtWebKitWidgets.QWebPage.Reload).setVisible(False)
        else:
            self.stackedWidget.setCurrentIndex(1)


###### Outils ############################################
##########################################################

    @QtCore.pyqtSlot()
    def showTools(self):
        self.stackedWidget.setCurrentIndex(0)
        pyDir = os.path.abspath(os.path.dirname(__file__))
        baseUrl = QtCore.QUrl.fromLocalFile(os.path.join(pyDir, "static/"))
        self.webViewBiblio.setHtml(ToolExporterRepository["html"](), baseUrl)
        self.webViewBiblio.page().mainFrame().addToJavaScriptWindowObject(
            "main", self)
        # self.webViewBiblio.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
        # self.webInspector = QtWebKit.QWebInspector(self)
        # self.webInspector.setPage(self.webViewBiblio.page())
        # self.webInspector.setVisible(True)
        # self.verticalLayout_13.addWidget(self.webInspector)

    @QtCore.pyqtSlot(result=str)
    def dataRecipes(self):
        # f = open(recipeData_file, 'w')
        # f.write(self.recipesSummary)
        self.listdir(recettes_dir)
        return self.recipesSummary

    @QtCore.pyqtSlot(result=str)
    def dataProfiles(self):
        return self.mashProfileExport.exportJson(ImportBase().listeMashes)

    @QtCore.pyqtSlot(result=str)
    def dataIngredients(self):
        return ImportBase().exportjson()

    @QtCore.pyqtSlot(result=str)
    def dataPref(self):
        dic = {}
        dic["boilOffRate"] = settings.conf.value("BoilOffRate")
        dic["coolingLossRate"] = settings.conf.value("CoolingLoss")
        dic["grainTemp"] = settings.conf.value("GrainTemp")
        dic["fudgeFactor"] = settings.conf.value("FudgeFactor")
        dic["grainRetention"] = settings.conf.value("GrainRetention")
        dic = json.dumps(dic)
        return dic

    #Une fonction qui gère l'aperçu des couleurs.
    #Contient un tupple avec plusieurs références de couleurs, classées par rang selon la valeur SRM.
    #################################################################################################
    # def colorPreview (self) :
    #     self.colorTuppleSrm = ('FFE699', 'FFD878', 'FFCA5A', 'FFBF42', 'FBB123', 'F8A600', 'F39C00', 'EA8F00', 'E58500', 'DE7C00', 'D77200', 'CF6900', 'CB6200', 'C35900','BB5100', 'B54C00', 'B04500', 'A63E00', 'A13700', '9B3200', '952D00', '8E2900', '882300', '821E00', '7B1A00', '771900', '701400', '6A0E00', '660D00','5E0B00','5A0A02','600903', '520907', '4C0505', '470606', '440607', '3F0708', '3B0607', '3A070B', '36080A')

    #     colorRef= round(self.recipe.compute_EBC()/1.97)

    #     if colorRef >= 30 :
    #         color = "#" + self.colorTuppleSrm[30]
    #     elif colorRef <= 1 :
    #         color = "#" + self.colorTuppleSrm[0]
    #     else :
    #         color = "#" + self.colorTuppleSrm[colorRef-1]
    #     self.widgetColor.setStyleSheet("background-color :" + color)

    def liste_fichiers_recettes(self, rootdir):
        for root, subFolders, files in os.walk(rootdir):
            for file2 in files:
                yield (file2, os.path.join(root, file2))

    def listdir(self, rootdir):
        summaries = []
        for filename, recipe in self.liste_fichiers_recettes(rootdir):
            try:
                summaries.append(self.jsonRecipeLib(recipe))
            except:
                logger.debug("le fichier %s n'est pas une recette" % (recipe))
        self.recipesSummary = "[" + ",".join(summaries) + "]"
        logger.debug("%s recettes détectées" % (len(summaries)))

    def jsonRecipeLib(self, recipe):
        self.s = recipe
        self.recipe = Recipe.parse(recipe)
        data = self.recipe.export("json")
        data = data[1:-1]
        return data

    def initRep(self):
        home = QtCore.QDir(home_dir)
        config = QtCore.QDir(config_dir)
        logger.debug(config)
        if not config.exists():
            home.mkpath(config_dir)
        else:
            pass
        database = QtCore.QFile(database_file)
        if not database.exists():
            database.copy(database_root, database_file)
        else:
            pass
        recettes = QtCore.QFile(recettes_dir)
        if not recettes.exists():
            try:
                shutil.copytree(samples_dir, samples_target)
            except:
                home.mkpath(recettes_dir)
        mash = QtCore.QFile(mash_file)
        if not mash.exists():
            mash.copy(mash_root, mash_file)
        else:
            pass
        journal = QtCore.QFile(journal_file)
        if not journal.exists():
            journal.copy(journal_root, journal_file)
        else:
            pass

        # on configure des valeurs par défaut
        if not settings.conf.contains("BoilOffRate"):
            settings.conf.setValue("BoilOffRate", 10)
        if not settings.conf.contains("CoolingLoss"):
            settings.conf.setValue("CoolingLoss", 5)
        if not settings.conf.contains("GrainTemp"):
            settings.conf.setValue("GrainTemp", 20)
        if not settings.conf.contains("FudgeFactor"):
            settings.conf.setValue("FudgeFactor", 1.7)
        if not settings.conf.contains("GrainRetention"):
            settings.conf.setValue("GrainRetention", 1)
        if not settings.conf.contains("Menus"):
            settings.conf.setValue("Menus", "button")

    def prefReload(self):
        if platform == 'win32':
            recettes_dir = settings.conf.value("pathWin32")
        else:
            recettes_dir = settings.conf.value("pathUnix")
        self.initRep()
        self.initKintoClient()
        self.listdir(recettes_dir)
        self.showLib()

    def initKintoClient(self):
        self.kinto_client = None
        kinto_url = settings.conf.value("KintoServerUrl")
        if kinto_url is not None and kinto_url != "":
            kinto_bucket = settings.conf.value("KintoDefaultBucket")
            kinto_credentials = (settings.conf.value("KintoBasicUserCred"),
                                 settings.conf.value("KintoBasicPasswordCred"))
            logger.debug(kinto_credentials)
            if kinto_bucket is None or kinto_bucket == "":
                kinto_bucket = "joliebulle"
                logger.debug("using default bucket 'joliebulle'")
            try:
                tmp_client = Client(server_url=kinto_url,
                                    auth=kinto_credentials)
                tmp_client.create_bucket(id=kinto_bucket, if_not_exists=True)
                self.kinto_client = Client(server_url=kinto_url,
                                           bucket=kinto_bucket,
                                           auth=kinto_credentials)

                # Création des collections
                self.kinto_client.create_collection(id='recipes',
                                                    if_not_exists=True)
                self.kinto_client.create_collection(id='ingredients',
                                                    if_not_exists=True)

                logger.info("Synchronize with kinto server at :" +
                            repr(self.kinto_client))
            except Exception as e:
                logger.warn("Failed to initialize Kinto synchronisation: " +
                            repr(e))

    def syncRecipes(self):
        recipes_collection = 'recipes'
        if self.kinto_client is None:
            return

        # Sync local recipes
        id_recettes_locales = []
        for filename, full_filename in self.liste_fichiers_recettes(
                recettes_dir):
            recipe_id = filename.replace('.xml', '')
            id_recettes_locales.append(recipe_id)
            try:
                remote_recipe = self.kinto_client.get_record(
                    id=recipe_id, collection=recipes_collection)
                logger.debug("recipe " + recipe_id +
                             " already exists on server, update if needed")
                remote_timestamp = int(remote_recipe['data']['last_modified'] /
                                       1000)
                local_timestamp = int(os.path.getmtime(full_filename))
                logger.debug(
                    str(remote_timestamp) + " " + str(local_timestamp))
                if remote_timestamp < local_timestamp:
                    logger.info("Mise à jour de la recette distante " +
                                recipe_id)
                    # La recette distant doit être mise à jour
                    local_recipe = Recipe.parse(full_filename)
                    data = local_recipe.export("dict")
                    ret = self.kinto_client.update_record(
                        id=recipe_id, collection=recipes_collection, data=data)
                    new_timestamp = ret['data']['last_modified']
                    #MAJ du mtime du fichier pour marquer la synchronisation
                    os.utime(full_filename,
                             times=(int(new_timestamp / 1000),
                                    int(new_timestamp / 1000)))
                if remote_timestamp > local_timestamp:
                    # La recette distante doit être mise à jour
                    logger.info("Mise à jour de la recette locale " +
                                recipe_id)
                    new_recipe = Recipe.parse(remote_recipe['data'], "dict")
                    self.doEnregistrerRecette(new_recipe, full_filename)
                    os.utime(full_filename,
                             times=(int(remote_timestamp),
                                    int(remote_timestamp)))
            except KintoException:
                logger.debug("recipe " + recipe_id +
                             " doesn't exists on server, sending it")
                new_recipe = Recipe.parse(full_filename)
                data = new_recipe.export("dict")
                ret = self.kinto_client.create_record(
                    id=recipe_id, collection=recipes_collection, data=data)
                new_timestamp = ret['data']['last_modified']
                #MAJ du mtime du fichier pour marquer la synchronisation
                os.utime(full_filename,
                         times=(int(new_timestamp / 1000),
                                int(new_timestamp / 1000)))

        recipes = self.kinto_client.get_records(collection='recipes')
        for recipe in recipes:
            if recipe['id'] not in id_recettes_locales:
                #Recette distante non présente localement
                new_recipe = Recipe.parse(recipe, "dict")
                fullname = recipe['id'] + ".xml"
                logger.debug("Création de la recette " + fullname)
                self.doEnregistrerRecette(new_recipe,
                                          os.path.join(recettes_dir, fullname))
                os.utime(full_filename,
                         times=(int(remote_timestamp), int(remote_timestamp)))

    @QtCore.pyqtSlot()
    def switchToLibrary(self):
        self.stackedWidget.setCurrentIndex(0)
        # self.viewRecipeLib(self.s)

    def switchToMash(self):
        self.stackedWidget.setCurrentIndex(2)

    def restoreDataBase(self):
        home = QtCore.QDir(home_dir)
        config = QtCore.QDir(config_dir)
        database = QtCore.QFile(database_file)
        confirmation = QtWidgets.QMessageBox.question(
            self, self.tr("Remplacer la base ?"),
            self.
            tr("La base des ingrédients actuelle va être effacée et remplacée par la base originale. Toutes vos modifications vont être effacées. Un redémarrage de l'application sera nécessaire.<br> Continuer ?"
               ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        if (confirmation == QtWidgets.QMessageBox.Yes):
            database.remove(database_file)
            database.copy(database_root, database_file)
        else:

            pass

    def editGrains(self):
        self.dlgEditG.setModal(True)
        self.dlgEditG.setModel()
        self.dlgEditG.show()

    def editHoublons(self):
        self.dlgEditH.setModal(True)
        self.dlgEditH.setModel()
        self.dlgEditH.show()

    def editDivers(self):
        self.dlgEditD.setModal(True)
        self.dlgEditD.setModel()
        self.dlgEditD.show()

    def editLevures(self):
        self.dlgEditY.setModal(True)
        self.dlgEditY.setModel()
        self.dlgEditY.show()

    @QtCore.pyqtSlot(float, float, float, float)
    def preBoilCheck(self, volPreBoil, preBoilSg, GU, volume):
        self.dlgPreBoil = DialogPreBoil(self)
        self.dlgPreBoil.setData(volPreBoil, preBoilSg, GU, volume)
        self.dlgPreBoil.setModal(True)
        self.dlgPreBoil.show()

    def dialogPreferences(self):
        self.dlgPref.setModal(True)
        self.dlgPref.show()

    def importBeerXML(self):
        fichierBeerXML = self.s
        try:
            self.recipe = Recipe.parse(fichierBeerXML)
            self.currentRecipeMash = self.recipe.mash

        except:
            errors = Errors()
            errors.warningXml()

    def about(self):
        about = DialogAbout(self)
        about.show()

    def doEnregistrerRecette(self, recipe, destination):
        recipeFile = QtCore.QFile(destination)
        if recipeFile.open(QtCore.QIODevice.WriteOnly):
            try:
                stream = QtCore.QTextStream(recipeFile)
                stream.setCodec("UTF-8")
                stream << recipe.export("beerxml")
            finally:
                recipeFile.close()
        else:
            # TODO : Prévenir l'utilisateur en cas d'échec de l'enregistrement
            pass

    def enregistrerRecette(self, destination):
        self.doEnregistrerRecette(self.recipe, destination)
        self.fileSaved = True

    def enregistrer(self):
        if self.recipe.name != self.lineEditRecette.text():
            self.nameChanged = True
        else:
            self.nameChanged = False

        self.recipe.name = self.lineEditRecette.text()
        self.recipe.style = self.lineEditGenre.text()
        self.recipe.brewer = self.lineEditBrewer.text()
        self.recipe.boil = self.spinBoxBoil.value()
        if not self.s:
            destination = recettes_dir + "/" + self.recipe.name.replace(
                '/', ' ') + ".xml"
            if os.path.exists(destination):
                errors = Errors()
                errors.warningExistingPath()
                self.fileSaved = False
            else:
                self.s = destination
                self.enregistrerRecette(destination)
        else:
            self.enregistrerRecette(self.s)

    def enregistrerSous(self):
        self.s = QtGui.QFileDialog.getSaveFileName(
            self, self.tr("Enregistrer dans un fichier"),
            recettes_dir + "/" + self.recipe.name.replace('/', ' ') + ".xml",
            "BeerXML (*.xml)")
        self.enregistrerRecette(self.s)

    @QtCore.pyqtSlot(str)
    def copyBbcode(self, bbcode):
        app.clipboard().setText(bbcode)

    def importIng(self):
        s = QtWidgets.QFileDialog.getOpenFileName(
            self,
            self.tr("Ouvrir un fichier"),
            home_dir,
        )
        if not s:
            pass
        else:
            self.importIngList = ImportIng()
            self.importIngList.parseFile(s)

    def mashComboChanged(self):
        #on remet le verrou à 0, il va falloir recalculer en repassant en brewday mode
        self.brewdayLock = 0
        try:
            i = self.comboBoxMashProfiles.currentIndex()
            self.currentMash = ImportBase().listeMashes[i]
        except:
            self.currentMash = self.currentRecipeMash
        if i == -1:
            self.currentMash = Mash()
        self.recipe.mash = self.currentMash

    def seeMash(self):
        self.switchToMash()
        index = self.listWidgetMashProfiles.currentRow()
        i = self.listWidgetSteps.currentRow()
        self.listWidgetMashProfiles.clear()
        self.listWidgetSteps.clear()

        self.numMash = len(ImportBase().listeMashes)
        #self.numSteps = self.mashProfilesBase.numSteps
        self.popMashList()
        self.pushButtonMashEdit.setEnabled(False)
        self.pushButtonRemoveProfile.setEnabled(False)
        self.pushButtonStepRemove.setEnabled(False)
        self.pushButtonStepEdit.setEnabled(False)
        self.listWidgetMashProfiles.setCurrentRow(index)
        self.listWidgetSteps.setCurrentRow(i)

    def popMashList(self):
        self.listWidgetMashProfiles.clear()
        for mash in ImportBase().listeMashes:
            self.listWidgetMashProfiles.addItem(mash.name)

    def mashClicked(self):
        self.listWidgetSteps.clear()
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            mash = ImportBase().listeMashes[index]
            for step in mash.listeSteps:
                self.listWidgetSteps.addItem(step.name)

            self.labelStepName.setTextFormat(QtCore.Qt.RichText)
            self.labelMashName.setText("<b>" + mash.name + "</b>")
            self.labelMashPh.setText("%.1f" % float(mash.ph))
            #        self.labelMashGrainTemp.setText("%.1f" %float(self.dicMashDetail['grainTemp']))
            #        self.labelMashTunTemp.setText("%.1f" %float(self.dicMashDetail['tunTemp']))
            try:
                self.labelMashSpargeTemp.setText("%.1f" %
                                                 float(mash.spargeTemp))
            except:
                pass
            try:
                self.listWidgetSteps.setCurrentRow(0)
            except:
                pass

    #        print(self.dicMashDetail)
            self.pushButtonMashEdit.setEnabled(True)
            self.pushButtonRemoveProfile.setEnabled(True)

    def mashDetails(self):
        self.dlgMashDetail = DialogMashDetail(self)
        self.dlgMashDetail.setModal(True)
        self.dlgMashDetail.show()
        self.dlgMashDetail.setFields(self.currentMash)
        self.dlgMashDetail.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)

    def stepDetails(self):
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                try:
                    selected_step = selected_mash.listeSteps[i]
                    self.labelStepName.setTextFormat(QtCore.Qt.RichText)
                    self.labelStepName.setText("<b>" + selected_step.name +
                                               "</b>")
                    self.labelStepType.setText(selected_step.type)
                    self.labelStepTemp.setText(
                        MashStepView.temp_to_display(selected_step.temp))
                    self.labelStepTime.setText(
                        MashStepView.time_to_display(selected_step.time))
                    self.pushButtonStepRemove.setEnabled(True)
                    self.pushButtonStepEdit.setEnabled(True)
                except:
                    pass

    def stepEdit(self):
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                selected_step = selected_mash.listeSteps[i]

                self.dlgStep.show()
                self.dlgStep.fields(selected_step)

    def stepReload(self, step):
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                selected_step = selected_mash.listeSteps[i]

                selected_step.name = step.name
                selected_step.type = step.type
                selected_step.temp = step.temp
                selected_step.time = step.time
                self.seeMash()
                self.stepDetails()
                self.listWidgetMashProfiles.setCurrentRow(index)
                self.listWidgetSteps.setCurrentRow(i)

    def removeStep(self):
        index = self.listWidgetMashProfiles.currentRow()
        if index > -1:
            selected_mash = ImportBase().listeMashes[index]
            i = self.listWidgetSteps.currentRow()
            if i > -1:
                item = self.listWidgetSteps.currentItem()
                del selected_mash.listeSteps[i]
                # self.listWidgetSteps.clearSelection()
                #self.listWidgetSteps.takeItem(item)
                #On force la sélection sur la ligne précédente
                self.listWidgetSteps.setCurrentRow(i - 1)
                self.seeMash()

    def addStep(self):
        index = self.listWidgetMashProfiles.currentRow()
        selected_mash = ImportBase().listeMashes[index]
        i = self.listWidgetSteps.currentRow()
        step = MashStep()
        step.name = 'Nouveau palier'
        step.type = 'Infusion'
        step.time = '0'
        step.temp = '0'
        step.vol = '0'
        selected_mash.listeSteps.append(step)

        self.listWidgetMashProfiles.setCurrentRow(index)
        self.seeMash()
        self.stepDetails()
        self.listWidgetMashProfiles.setCurrentRow(index)
        # self.listWidgetSteps.setCurrentRow(i-1)
        # self.stepEdit()

    def mashEdit(self):
        index = self.listWidgetMashProfiles.currentRow()
        selected_mash = ImportBase().listeMashes[index]
        self.dlgMash.show()
        self.dlgMash.fields(selected_mash)

    def mashReload(self, mash):
        #on remet le verrou à 0, il va falloir recalculer en repassant en brewday mode
        self.brewdayLock = 0
        f = self.listWidgetMashProfiles.currentRow()
        selected_mash = ImportBase().listeMashes[f]
        selected_mash.name = mash.name
        selected_mash.ph = mash.ph
        selected_mash.grainTemp = 20
        selected_mash.tunTemp = 20
        selected_mash.spargeTemp = mash.spargeTemp
        self.popMashList()
        self.listWidgetMashProfiles.setCurrentRow(f)

    def addMash(self):
        new_mash = Mash()
        new_mash.name = 'Nouveau profil'
        new_mash.grainTemp = '0'
        new_mash.tunTemp = '0'
        new_mash.spargeTemp = '78'
        new_mash.ph = 5.4
        new_step = MashStep()
        new_step.name = 'Nouveau Palier'
        new_step.type = 'Infusion'
        new_step.time = '0'
        new_step.temp = '0'
        new_mash.listeSteps.append(new_step)
        ImportBase().listeMashes.append(new_mash)
        self.seeMash()
        self.listWidgetMashProfiles.setCurrentRow(
            len(ImportBase().listeMashes) - 1)

    def removeMash(self):
        i = self.listWidgetMashProfiles.currentRow()
        del ImportBase().listeMashes[i]
        self.seeMash()
        self.listWidgetSteps.clear()

    def mashRejected(self):
        self.showLib()

    def saveProfile(self):
        self.mashProfileExport.export(ImportBase().listeMashes)
        self.mashProfileExport.enregistrer(mash_file)

    @QtCore.pyqtSlot()
    def printRecipe(self):
        printer = QtPrintSupport.QPrinter()
        dialog = QtPrintSupport.QPrintDialog(printer)
        dialog.setModal(True)
        dialog.setWindowTitle("Print Document")
        if dialog.exec_() == True:
            self.webViewBiblio.print(printer)
            # document=QtGui.QTextDocument()
            # stringHtml=self.recipe.export("print")
            # document.setHtml(stringHtml)
            # document.print(printer)

    @QtCore.pyqtSlot()
    def printBrewday(self):
        printer = QtPrintSupport.QPrinter()
        dialog = QtPrintSupport.QPrintDialog(printer)
        dialog.setModal(True)
        dialog.setWindowTitle("Print Document")
        if dialog.exec_() == True:
            self.webViewBrewday.print(printer)