Skip to content
Snippets Groups Projects
Commit c3a028b3 authored by Simon Kindhauser's avatar Simon Kindhauser
Browse files

add url type

parent 5feb6805
No related branches found
No related tags found
2 merge requests!11Release/v0.12.0,!4add url type
from pathlib import Path
from base.url.url_view_factory import UrlViewFactory
from blackfennec.extension.extension import Extension
from blackfennec.extension.extension_api import ExtensionApi
from blackfennec.interpretation.specification import Specification
......@@ -20,13 +22,15 @@ class BaseExtension(Extension):
DateTimeViewFactory(),
FileViewFactory(self._api.document_registry),
ImageViewFactory(self._api.document_registry),
UrlViewFactory(self._api.ui_service)
]
def register_types(self):
self.types = [
DateTime.TYPE,
self._api.type_loader.load(BASE_NAME + '/file/file.json'),
self._api.type_loader.load(BASE_NAME + '/image/image.json')
self._api.type_loader.load(BASE_NAME + '/image/image.json'),
self._api.type_loader.load(BASE_NAME + '/url/url.json')
]
self._api.type_registry.register_type(DateTime.TYPE)
......
{
"super": { "$ref": "bftype://String"},
"type": "Url",
"pattern": "^(?:https?://)?(?:[\\w]+\\.)(?:\\.?[\\w]{2,})+$",
"default": "https://blackfennec.org"
}
\ No newline at end of file
using Gtk 4.0;
using Adw 1;
template UrlPreview : Box {
Frame {
focusable: false;
visible: true;
valign: center;
margin-top: 8;
margin-bottom: 8;
TextView _value {
height-request: 32;
width-request: 128;
hexpand: true;
focusable: true;
wrap-mode: word_char;
left-margin: 8;
right-margin: 8;
top-margin: 8;
bottom-margin: 8;
}
}
Button {
focusable: true;
receives-default: true;
halign: end;
valign: center;
clicked => _on_open_clicked();
margin-bottom: 8;
margin-top: 8;
margin-start: 8;
icon-name: "web-browser-symbolic";
styles [
"flat",
"circular"
]
}
}
import os
from pathlib import Path
from base.url.url_view_model import UrlViewModel
from blackfennec.actions.context import Context
from blackfennec.facade.ui_service.message import Message
from blackfennec.facade.ui_service.ui_service import UiService
from blackfennec.util.change_notification import ChangeNotification
from gi.repository import Gtk
BASE_DIR = Path(__file__).resolve().parent
UI_TEMPLATE = str(BASE_DIR.joinpath('url_preview.ui'))
@Gtk.Template(filename=UI_TEMPLATE)
class UrlPreview(Gtk.Box):
__gtype_name__ = 'UrlPreview'
_value = Gtk.Template.Child()
def __init__(self, view_model: UrlViewModel, ui_service: UiService):
super().__init__()
self._view_model = view_model
self._ui_service = ui_service
self._view_model.bind(changed=self._update_value)
self._buffer_listener_paused = False
buffer = self._value.get_buffer()
buffer.set_text(self._view_model.url)
buffer.set_enable_undo(False)
buffer.connect('changed', self._on_buffer_changed)
self.connect('notify::active', self._on_activate)
@property
def text(self):
buffer = self._value.get_buffer()
start, end = buffer.get_bounds()
return buffer.get_text(start, end, False)
@text.setter
def text(self, text):
buffer = self._value.get_buffer()
self._buffer_listener_paused = True
start, end = buffer.get_bounds()
if buffer.get_text(start, end, False) == text:
return
buffer.set_text(text, len(text))
def _on_activate(self, unused_sender):
self._value.activate()
def _on_buffer_changed(self, buffer):
# This is a workaround for a bug in Gtk.TextView
# when we set the text in the buffer, the buffer-changed signal is
# emitted, twice. The first time with an empty url.
if self._buffer_listener_paused:
self._buffer_listener_paused = False
return
self._view_model.value = self.text
def _update_value(self, unused_sender, notification: ChangeNotification):
self.text = notification.new_value
@Gtk.Template.Callback()
def _on_open_clicked(self, unused_sender):
os.system(f"xdg-open {self.text}")
context = Context(self)
message = Message('Url opened')
self._ui_service.show_message(context, message)
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="UrlPreview" parent="GtkBox">
<child>
<object class="GtkFrame">
<property name="focusable">false</property>
<property name="visible">true</property>
<property name="valign">center</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<child>
<object class="GtkTextView" id="_value">
<property name="height-request">32</property>
<property name="width-request">128</property>
<property name="hexpand">true</property>
<property name="focusable">true</property>
<property name="wrap-mode">word-char</property>
<property name="left-margin">8</property>
<property name="right-margin">8</property>
<property name="top-margin">8</property>
<property name="bottom-margin">8</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton">
<property name="focusable">true</property>
<property name="receives-default">true</property>
<property name="halign">end</property>
<property name="valign">center</property>
<signal name="clicked" handler="_on_open_clicked"/>
<property name="margin-bottom">8</property>
<property name="margin-top">8</property>
<property name="margin-start">8</property>
<property name="icon-name">web-browser-symbolic</property>
<style>
<class name="flat"/>
<class name="circular"/>
</style>
</object>
</child>
</template>
</interface>
\ No newline at end of file
from base.url.url_view_model import UrlViewModel
from base.url.url_preview import UrlPreview
from blackfennec.interpretation.interpretation import Interpretation
from blackfennec.interpretation.specification import Specification
class UrlViewFactory:
def __init__(self, ui_service) -> None:
self._ui_service = ui_service
def satisfies(self, specification: Specification) -> bool:
return specification.is_request_for_preview
def create(self, interpretation: Interpretation) -> UrlPreview:
view_model = UrlViewModel(interpretation)
return UrlPreview(view_model, self._ui_service)
from blackfennec.util.change_notification_dispatch_mixin import ChangeNotificationDispatchMixin
class UrlViewModel(ChangeNotificationDispatchMixin):
def __init__(self, interpretation):
super().__init__()
self._url_string = interpretation.structure
self._url_string.bind(changed=self._dispatch_change_notification)
@property
def url(self) -> str:
"""Property for url"""
return self._url_string.value
@url.setter
def url(self, url: str):
# prevent event loop
if url == self.url:
return
self._url_string.value = url
import unittest
from blackfennec_doubles.structure.double_structure import StructureMock
from blackfennec_doubles.structure.double_map import MapMock
from blackfennec_doubles.structure.double_string import StringMock
from base.image.image import Image
class ImageTestSuite(unittest.TestCase):
def test_can_construct(self):
image = Image()
self.assertEqual(image.file_path, '')
self.assertTrue(image.file_type.startswith('image/'))
def test_can_construct_with_map(self):
data = dict()
data[Image.FILE_PATH_KEY] = StringMock('file_path')
data[Image.FILE_TYPE_KEY] = StringMock('file_type')
data_map = MapMock(data)
image = Image(data_map)
self.assertIsNotNone(image)
def test_can_construct_with_empty_map(self):
data = dict()
data_map = MapMock(data)
image = Image(data_map)
self.assertIsNotNone(image)
def test_deletion_of_key_after_construction(self):
data = dict()
data[Image.FILE_PATH_KEY] = StringMock('file_path')
data[Image.FILE_TYPE_KEY] = StringMock('image/mock')
data_map = MapMock(data)
image = Image(data_map)
del data[Image.FILE_PATH_KEY]
self.assertIsNone(image.file_path)
def test_file_path_getter(self):
data = dict()
data[Image.FILE_PATH_KEY] = StringMock('file_path')
data_map = MapMock(data)
image = Image(data_map)
self.assertEqual(image.file_path, data[Image.FILE_PATH_KEY].value)
def test_file_path_setter(self):
file_path = StringMock('file_path')
image = Image()
image.file_path = file_path.value
file_path.parent = image
self.assertEqual(image.file_path, file_path.value)
def test_file_type_getter(self):
data = dict()
data[Image.FILE_TYPE_KEY] = StringMock('image/mock')
data_map = MapMock(data)
image = Image(data_map)
self.assertEqual(image.file_type, data[Image.FILE_TYPE_KEY].value)
def test_file_type_setter(self):
file_type = StringMock('file_type')
image = Image()
image.file_type = file_type.value
file_type.parent = image
self.assertEqual(image.file_type, file_type.value)
def test_equal_unequal_elements(self):
data_map = MapMock({})
other_data_map = MapMock({Image.FILE_PATH_KEY: StructureMock('test')})
comp = Image(data_map)
other_comp = Image(other_data_map)
self.assertFalse(
comp == other_comp,
msg='Unequal elements are equal'
)
def test_not_equal_unequal_elements(self):
data_map = MapMock({})
other_data_map = MapMock({Image.FILE_PATH_KEY: StructureMock('test')})
comp = Image(data_map)
other_comp = Image(other_data_map)
self.assertTrue(
comp != other_comp,
msg='Unequal elements are equal'
)
def test_representation(self):
data = dict()
data[Image.FILE_PATH_KEY] = StringMock('file_path')
data[Image.FILE_TYPE_KEY] = StringMock('file_type')
data_map = MapMock(data)
image = Image(data_map)
expected = 'Image(file_path, file_type)'
self.assertEqual(repr(image), expected)
import pytest
from pathlib import Path
from blackfennec.type_system.string_type import StringType
from blackfennec.type_system.type_loader import TypeLoader
from blackfennec.structure.string import String
from blackfennec.type_system.type_registry import TypeRegistry
from blackfennec.document_system.document_factory import DocumentFactory
from blackfennec.document_system.mime_type.mime_type_registry import (
MimeTypeRegistry,
)
from blackfennec.document_system.mime_type.in_memory.in_memory_mime_type import (
InMemoryMimeType,
)
from blackfennec.document_system.mime_type.json.json_mime_type import (
JsonMimeType,
)
from blackfennec.document_system.mime_type.json.json_pointer_serializer import (
JsonPointerSerializer,
)
from blackfennec.document_system.mime_type.json.json_reference_serializer import (
JsonReferenceSerializer,
)
from blackfennec.structure.structure_serializer import (
StructureSerializer,
)
from blackfennec.document_system.resource_type.protocols.bftype_resource_type import (
BFTypeResourceType,
)
from blackfennec.document_system.resource_type.protocols.file_resource_type import (
FileResourceType,
)
from blackfennec.document_system.resource_type.resource_type_registry import (
ResourceTypeRegistry,
)
from blackfennec_doubles.document_system.double_document_registry import DocumentRegistryMock
BASE_NAME = Path(__file__).parent.parent.parent.as_posix()
@pytest.fixture
def type_registry():
type_registry = TypeRegistry()
type_registry.register_type(StringType())
return type_registry
@pytest.fixture
def document_factory(type_registry) -> DocumentFactory:
resource_type_registry = ResourceTypeRegistry()
resource_types = [
FileResourceType(),
BFTypeResourceType(type_registry),
]
for resource_type in resource_types:
for protocol in resource_type.protocols:
resource_type_registry.register_resource_type(
protocol, resource_type)
mime_type_registry = MimeTypeRegistry()
document_factory = DocumentFactory(
DocumentRegistryMock(), resource_type_registry, mime_type_registry)
reference_parser = JsonReferenceSerializer(
document_factory, JsonPointerSerializer)
structure_serializer = StructureSerializer(reference_parser)
mime_types = [
JsonMimeType(structure_serializer),
InMemoryMimeType(),
]
for mime_type in mime_types:
mime_type_registry.register_mime_type(
mime_type.mime_type_id, mime_type)
return document_factory
@pytest.fixture
def type_loader(document_factory, type_registry):
return TypeLoader(document_factory, type_registry)
@pytest.fixture
def type(type_loader):
return type_loader.load(BASE_NAME + "/base/url/url.json")
def test_can_load(type):
assert type is not None
def test_name_is_url(type):
assert type.name == "Url"
def test_super_type_is_string(type):
assert type.super.name == "String"
@pytest.mark.parametrize(
"structure",
[
String("https://blackfennec.org"),
String("http://blackfennec.org"),
String("blackfennec.org"),
]
)
def test_can_cover_url(type, structure):
assert type.calculate_coverage(structure).is_covered()
@pytest.mark.parametrize(
"structure",
[
String("https:/blackfennec"),
String("Black Fennec is great"),
]
)
def test_can_not_cover_url(type, structure):
assert not type.calculate_coverage(structure).is_covered()
import pytest
from blackfennec_doubles.document_system.double_document_registry import DocumentRegistryMock
from blackfennec_doubles.interpretation.double_interpretation import InterpretationMock
from blackfennec_doubles.interpretation.double_specification import SpecificationMock
from blackfennec_doubles.structure.double_string import StringMock
from base.url.url_preview import UrlPreview
from base.url.url_view_factory import UrlViewFactory
@pytest.fixture
def factory():
return UrlViewFactory(
DocumentRegistryMock())
def test_can_construct(factory):
assert factory
def test_can_create_url_preview(factory):
view = factory.create(
InterpretationMock(
structure=StringMock("https://blackfennec.org"),
specification=SpecificationMock(request_preview=True)))
assert isinstance(view, UrlPreview)
def test_satisfies_default(factory):
satisfies = factory.satisfies(SpecificationMock())
assert not satisfies
def test_does_not_satisfy_preview(factory):
satisfies = factory.satisfies(SpecificationMock(request_preview=True))
assert satisfies
import pytest
from blackfennec_doubles.interpretation.double_interpretation import InterpretationMock
from blackfennec_doubles.structure.double_map import MapMock
from blackfennec_doubles.structure.double_string import StringMock
from blackfennec_doubles.document_system.double_document_registry import DocumentRegistryMock
from base.url.url_view_model import UrlViewModel
@pytest.fixture
def url():
return StringMock('https://blackfennec.org')
@pytest.fixture
def interpretation(url):
return InterpretationMock(url)
@pytest.fixture
def view_model(interpretation):
return UrlViewModel(interpretation)
def test_can_construct(view_model):
assert view_model
def test_can_get_url(view_model):
assert view_model.url == 'https://blackfennec.org'
def test_can_set_url(view_model):
view_model.url = 'https://blackfennec.org/new_url'
assert view_model.url == 'https://blackfennec.org/new_url'
def test_does_not_set_url_twice(view_model):
view_model.url = 'https://blackfennec.org'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment