Show More
@@ -207,9 +207,18 b' define([' | |||||
207 | }); |
|
207 | }); | |
208 |
|
208 | |||
209 | this.events.on('spec_changed.Kernel', function(event, data) { |
|
209 | this.events.on('spec_changed.Kernel', function(event, data) { | |
210 |
that. |
|
210 | that.metadata.kernelspec = | |
|
211 | {name: data.name, display_name: data.display_name}; | |||
|
212 | }); | |||
|
213 | ||||
|
214 | this.events.on('kernel_ready.Kernel', function(event, data) { | |||
|
215 | var kinfo = data.kernel.info_reply | |||
|
216 | var langinfo = kinfo.language_info || {}; | |||
|
217 | if (!langinfo.name) langinfo.name = kinfo.language; | |||
|
218 | ||||
|
219 | that.metadata.language_info = langinfo; | |||
211 | // Mode 'null' should be plain, unhighlighted text. |
|
220 | // Mode 'null' should be plain, unhighlighted text. | |
212 |
cm_mode = |
|
221 | var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null' | |
213 | that.set_codemirror_mode(cm_mode); |
|
222 | that.set_codemirror_mode(cm_mode); | |
214 | }); |
|
223 | }); | |
215 |
|
224 | |||
@@ -329,21 +338,11 b' define([' | |||||
329 | md: this.metadata, |
|
338 | md: this.metadata, | |
330 | callback: function (md) { |
|
339 | callback: function (md) { | |
331 | that.metadata = md; |
|
340 | that.metadata = md; | |
332 |
}, |
|
341 | }, | |
333 | name: 'Notebook', |
|
342 | name: 'Notebook', | |
334 | notebook: this, |
|
343 | notebook: this, | |
335 | keyboard_manager: this.keyboard_manager}); |
|
344 | keyboard_manager: this.keyboard_manager}); | |
336 | }; |
|
345 | }; | |
337 |
|
||||
338 | Notebook.prototype.set_kernelspec_metadata = function(ks) { |
|
|||
339 | var tostore = {}; |
|
|||
340 | $.map(ks, function(value, field) { |
|
|||
341 | if (field !== 'argv' && field !== 'env') { |
|
|||
342 | tostore[field] = value; |
|
|||
343 | } |
|
|||
344 | }); |
|
|||
345 | this.metadata.kernelspec = tostore; |
|
|||
346 | } |
|
|||
347 |
|
346 | |||
348 | // Cell indexing, retrieval, etc. |
|
347 | // Cell indexing, retrieval, etc. | |
349 |
|
348 | |||
@@ -1811,6 +1810,14 b' define([' | |||||
1811 | this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec); |
|
1810 | this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec); | |
1812 | } |
|
1811 | } | |
1813 |
|
1812 | |||
|
1813 | // Set the codemirror mode from language_info metadata | |||
|
1814 | if (this.metadata.language_info !== undefined) { | |||
|
1815 | var langinfo = this.metadata.language_info; | |||
|
1816 | // Mode 'null' should be plain, unhighlighted text. | |||
|
1817 | var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null' | |||
|
1818 | this.set_codemirror_mode(cm_mode); | |||
|
1819 | } | |||
|
1820 | ||||
1814 | // Only handle 1 worksheet for now. |
|
1821 | // Only handle 1 worksheet for now. | |
1815 | var worksheet = content.worksheets[0]; |
|
1822 | var worksheet = content.worksheets[0]; | |
1816 | if (worksheet !== undefined) { |
|
1823 | if (worksheet !== undefined) { |
@@ -46,6 +46,7 b' define([' | |||||
46 | this.username = "username"; |
|
46 | this.username = "username"; | |
47 | this.session_id = utils.uuid(); |
|
47 | this.session_id = utils.uuid(); | |
48 | this._msg_callbacks = {}; |
|
48 | this._msg_callbacks = {}; | |
|
49 | this.info_reply = {}; // kernel_info_reply stored here after starting | |||
49 |
|
50 | |||
50 | if (typeof(WebSocket) !== 'undefined') { |
|
51 | if (typeof(WebSocket) !== 'undefined') { | |
51 | this.WebSocket = WebSocket; |
|
52 | this.WebSocket = WebSocket; | |
@@ -398,7 +399,8 b' define([' | |||||
398 | this.events.trigger('kernel_starting.Kernel', {kernel: this}); |
|
399 | this.events.trigger('kernel_starting.Kernel', {kernel: this}); | |
399 | // get kernel info so we know what state the kernel is in |
|
400 | // get kernel info so we know what state the kernel is in | |
400 | var that = this; |
|
401 | var that = this; | |
401 | this.kernel_info(function () { |
|
402 | this.kernel_info(function (reply) { | |
|
403 | that.info_reply = reply.content; | |||
402 | that.events.trigger('kernel_ready.Kernel', {kernel: that}); |
|
404 | that.events.trigger('kernel_ready.Kernel', {kernel: that}); | |
403 | }); |
|
405 | }); | |
404 | }; |
|
406 | }; | |
@@ -925,7 +927,8 b' define([' | |||||
925 | } else if (execution_state === 'starting') { |
|
927 | } else if (execution_state === 'starting') { | |
926 | this.events.trigger('kernel_starting.Kernel', {kernel: this}); |
|
928 | this.events.trigger('kernel_starting.Kernel', {kernel: this}); | |
927 | var that = this; |
|
929 | var that = this; | |
928 | this.kernel_info(function () { |
|
930 | this.kernel_info(function (reply) { | |
|
931 | that.info_reply = reply.content; | |||
929 | that.events.trigger('kernel_ready.Kernel', {kernel: that}); |
|
932 | that.events.trigger('kernel_ready.Kernel', {kernel: that}); | |
930 | }); |
|
933 | }); | |
931 |
|
934 |
@@ -36,18 +36,9 b' def _pythonfirst(s):' | |||||
36 | class KernelSpec(HasTraits): |
|
36 | class KernelSpec(HasTraits): | |
37 | argv = List() |
|
37 | argv = List() | |
38 | display_name = Unicode() |
|
38 | display_name = Unicode() | |
39 | language = Unicode() |
|
|||
40 | codemirror_mode = Any() # can be unicode or dict |
|
|||
41 | pygments_lexer = Unicode() |
|
|||
42 | env = Dict() |
|
39 | env = Dict() | |
43 | resource_dir = Unicode() |
|
40 | resource_dir = Unicode() | |
44 |
|
41 | |||
45 | def _codemirror_mode_default(self): |
|
|||
46 | return self.language |
|
|||
47 |
|
||||
48 | def _pygments_lexer_default(self): |
|
|||
49 | return self.language |
|
|||
50 |
|
||||
51 | @classmethod |
|
42 | @classmethod | |
52 | def from_resource_dir(cls, resource_dir): |
|
43 | def from_resource_dir(cls, resource_dir): | |
53 | """Create a KernelSpec object by reading kernel.json |
|
44 | """Create a KernelSpec object by reading kernel.json | |
@@ -63,12 +54,7 b' class KernelSpec(HasTraits):' | |||||
63 | d = dict(argv=self.argv, |
|
54 | d = dict(argv=self.argv, | |
64 | env=self.env, |
|
55 | env=self.env, | |
65 | display_name=self.display_name, |
|
56 | display_name=self.display_name, | |
66 | language=self.language, |
|
|||
67 | ) |
|
57 | ) | |
68 | if self.codemirror_mode != self.language: |
|
|||
69 | d['codemirror_mode'] = self.codemirror_mode |
|
|||
70 | if self.pygments_lexer != self.language: |
|
|||
71 | d['pygments_lexer'] = self.pygments_lexer |
|
|||
72 |
|
58 | |||
73 | return d |
|
59 | return d | |
74 |
|
60 | |||
@@ -118,12 +104,8 b' class KernelSpecManager(HasTraits):' | |||||
118 | process. This will put its informatino in the user kernels directory. |
|
104 | process. This will put its informatino in the user kernels directory. | |
119 | """ |
|
105 | """ | |
120 | return {'argv': make_ipkernel_cmd(), |
|
106 | return {'argv': make_ipkernel_cmd(), | |
121 | 'display_name': 'IPython (Python %d)' % (3 if PY3 else 2), |
|
107 | 'display_name': 'IPython (Python %d)' % (3 if PY3 else 2), | |
122 | 'language': 'python', |
|
108 | } | |
123 | 'codemirror_mode': {'name': 'ipython', |
|
|||
124 | 'version': sys.version_info[0]}, |
|
|||
125 | 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2), |
|
|||
126 | } |
|
|||
127 |
|
109 | |||
128 | @property |
|
110 | @property | |
129 | def _native_kernel_resource_dir(self): |
|
111 | def _native_kernel_resource_dir(self): |
@@ -9,7 +9,6 b' from IPython.kernel import kernelspec' | |||||
9 |
|
9 | |||
10 | sample_kernel_json = {'argv':['cat', '{connection_file}'], |
|
10 | sample_kernel_json = {'argv':['cat', '{connection_file}'], | |
11 | 'display_name':'Test kernel', |
|
11 | 'display_name':'Test kernel', | |
12 | 'language':'bash', |
|
|||
13 | } |
|
12 | } | |
14 |
|
13 | |||
15 | class KernelSpecTests(unittest.TestCase): |
|
14 | class KernelSpecTests(unittest.TestCase): | |
@@ -39,8 +38,6 b' class KernelSpecTests(unittest.TestCase):' | |||||
39 | self.assertEqual(ks.resource_dir, self.sample_kernel_dir) |
|
38 | self.assertEqual(ks.resource_dir, self.sample_kernel_dir) | |
40 | self.assertEqual(ks.argv, sample_kernel_json['argv']) |
|
39 | self.assertEqual(ks.argv, sample_kernel_json['argv']) | |
41 | self.assertEqual(ks.display_name, sample_kernel_json['display_name']) |
|
40 | self.assertEqual(ks.display_name, sample_kernel_json['display_name']) | |
42 | self.assertEqual(ks.language, sample_kernel_json['language']) |
|
|||
43 | self.assertEqual(ks.codemirror_mode, sample_kernel_json['language']) |
|
|||
44 | self.assertEqual(ks.env, {}) |
|
41 | self.assertEqual(ks.env, {}) | |
45 |
|
42 | |||
46 | def test_install_kernel_spec(self): |
|
43 | def test_install_kernel_spec(self): |
@@ -157,6 +157,7 b' class KernelInfoReply(Reference):' | |||||
157 | implementation_version = Version(min='2.1') |
|
157 | implementation_version = Version(min='2.1') | |
158 | language_version = Version(min='2.7') |
|
158 | language_version = Version(min='2.7') | |
159 | language = Unicode('python') |
|
159 | language = Unicode('python') | |
|
160 | language_info = Dict() | |||
160 | banner = Unicode() |
|
161 | banner = Unicode() | |
161 |
|
162 | |||
162 |
|
163 |
@@ -71,6 +71,11 b' class IPythonKernel(KernelBase):' | |||||
71 | implementation_version = release.version |
|
71 | implementation_version = release.version | |
72 | language = 'python' |
|
72 | language = 'python' | |
73 | language_version = sys.version.split()[0] |
|
73 | language_version = sys.version.split()[0] | |
|
74 | language_info = {'mimetype': 'text/x-python', | |||
|
75 | 'codemirror_mode': {'name': 'ipython', | |||
|
76 | 'version': sys.version_info[0]}, | |||
|
77 | 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2), | |||
|
78 | } | |||
74 | @property |
|
79 | @property | |
75 | def banner(self): |
|
80 | def banner(self): | |
76 | return self.shell.banner |
|
81 | return self.shell.banner |
@@ -60,6 +60,10 b' class Kernel(SingletonConfigurable):' | |||||
60 | def _ident_default(self): |
|
60 | def _ident_default(self): | |
61 | return unicode_type(uuid.uuid4()) |
|
61 | return unicode_type(uuid.uuid4()) | |
62 |
|
62 | |||
|
63 | # This should be overridden by wrapper kernels that implement any real | |||
|
64 | # language. | |||
|
65 | language_info = {} | |||
|
66 | ||||
63 | # Private interface |
|
67 | # Private interface | |
64 |
|
68 | |||
65 | _darwin_app_nap = Bool(True, config=True, |
|
69 | _darwin_app_nap = Bool(True, config=True, | |
@@ -453,6 +457,7 b' class Kernel(SingletonConfigurable):' | |||||
453 | 'implementation_version': self.implementation_version, |
|
457 | 'implementation_version': self.implementation_version, | |
454 | 'language': self.language, |
|
458 | 'language': self.language, | |
455 | 'language_version': self.language_version, |
|
459 | 'language_version': self.language_version, | |
|
460 | 'language_info': self.language_info, | |||
456 | 'banner': self.banner, |
|
461 | 'banner': self.banner, | |
457 | } |
|
462 | } | |
458 |
|
463 |
@@ -59,8 +59,8 b' class HTMLExporter(TemplateExporter):' | |||||
59 | return c |
|
59 | return c | |
60 |
|
60 | |||
61 | def from_notebook_node(self, nb, resources=None, **kw): |
|
61 | def from_notebook_node(self, nb, resources=None, **kw): | |
62 |
|
|
62 | langinfo = nb.metadata.get('language_info', {}) | |
63 |
lexer = |
|
63 | lexer = langinfo.get('pygments_lexer', langinfo.get('name', None)) | |
64 | self.register_filter('highlight_code', |
|
64 | self.register_filter('highlight_code', | |
65 | Highlight2HTML(pygments_lexer=lexer, parent=self)) |
|
65 | Highlight2HTML(pygments_lexer=lexer, parent=self)) | |
66 | return super(HTMLExporter, self).from_notebook_node(nb, resources, **kw) |
|
66 | return super(HTMLExporter, self).from_notebook_node(nb, resources, **kw) |
@@ -89,8 +89,8 b' class LatexExporter(TemplateExporter):' | |||||
89 | return c |
|
89 | return c | |
90 |
|
90 | |||
91 | def from_notebook_node(self, nb, resources=None, **kw): |
|
91 | def from_notebook_node(self, nb, resources=None, **kw): | |
92 |
|
|
92 | langinfo = nb.metadata.get('language_info', {}) | |
93 |
lexer = |
|
93 | lexer = langinfo.get('pygments_lexer', langinfo.get('name', None)) | |
94 | self.register_filter('highlight_code', |
|
94 | self.register_filter('highlight_code', | |
95 | Highlight2Latex(pygments_lexer=lexer, parent=self)) |
|
95 | Highlight2Latex(pygments_lexer=lexer, parent=self)) | |
96 | return super(LatexExporter, self).from_notebook_node(nb, resources, **kw) |
|
96 | return super(LatexExporter, self).from_notebook_node(nb, resources, **kw) |
@@ -27,7 +27,7 b' class Highlight2HTML(NbConvertBase):' | |||||
27 |
|
27 | |||
28 | def _default_language_changed(self, name, old, new): |
|
28 | def _default_language_changed(self, name, old, new): | |
29 | warn('Setting default_language in config is deprecated, ' |
|
29 | warn('Setting default_language in config is deprecated, ' | |
30 |
'please use |
|
30 | 'please use language_info metadata instead.') | |
31 | self.pygments_lexer = new |
|
31 | self.pygments_lexer = new | |
32 |
|
32 | |||
33 | def __call__(self, source, language=None, metadata=None): |
|
33 | def __call__(self, source, language=None, metadata=None): | |
@@ -61,7 +61,7 b' class Highlight2Latex(NbConvertBase):' | |||||
61 |
|
61 | |||
62 | def _default_language_changed(self, name, old, new): |
|
62 | def _default_language_changed(self, name, old, new): | |
63 | warn('Setting default_language in config is deprecated, ' |
|
63 | warn('Setting default_language in config is deprecated, ' | |
64 |
'please use |
|
64 | 'please use language_info metadata instead.') | |
65 | self.pygments_lexer = new |
|
65 | self.pygments_lexer = new | |
66 |
|
66 | |||
67 | def __call__(self, source, language=None, metadata=None, strip_verbatim=False): |
|
67 | def __call__(self, source, language=None, metadata=None, strip_verbatim=False): |
@@ -35,7 +35,7 b' class NbConvertBase(LoggingConfigurable):' | |||||
35 | ) |
|
35 | ) | |
36 |
|
36 | |||
37 | default_language = Unicode('ipython', config=True, |
|
37 | default_language = Unicode('ipython', config=True, | |
38 |
help='DEPRECATED default highlight language, please use |
|
38 | help='DEPRECATED default highlight language, please use language_info metadata instead') | |
39 |
|
39 | |||
40 | def __init__(self, **kw): |
|
40 | def __init__(self, **kw): | |
41 | super(NbConvertBase, self).__init__(**kw) |
|
41 | super(NbConvertBase, self).__init__(**kw) |
@@ -112,34 +112,16 b' JSON serialised dictionary containing the following keys and values:' | |||||
112 | - **display_name**: The kernel's name as it should be displayed in the UI. |
|
112 | - **display_name**: The kernel's name as it should be displayed in the UI. | |
113 | Unlike the kernel name used in the API, this can contain arbitrary unicode |
|
113 | Unlike the kernel name used in the API, this can contain arbitrary unicode | |
114 | characters. |
|
114 | characters. | |
115 | - **language**: The programming language which this kernel runs. This will be |
|
|||
116 | stored in notebook metadata. This may be used by syntax highlighters to guess |
|
|||
117 | how to parse code in a notebook, and frontends may eventually use it to |
|
|||
118 | identify alternative kernels that can run some code. |
|
|||
119 | - **codemirror_mode** (optional): The `codemirror mode <http://codemirror.net/mode/index.html>`_ |
|
|||
120 | to use for code in this language. This can be a string or a dictionary, as |
|
|||
121 | passed to codemirror config. This only needs to be specified if it does not |
|
|||
122 | match the value in *language*. |
|
|||
123 | - **pygments_lexer** (optional): The name of a `Pygments lexer <http://pygments.org/docs/lexers/>`_ |
|
|||
124 | to use for code in this language, as a string. This only needs to be specified |
|
|||
125 | if it does not match the value in *language*. |
|
|||
126 | - **env** (optional): A dictionary of environment variables to set for the kernel. |
|
115 | - **env** (optional): A dictionary of environment variables to set for the kernel. | |
127 | These will be added to the current environment variables before the kernel is |
|
116 | These will be added to the current environment variables before the kernel is | |
128 | started. |
|
117 | started. | |
129 | - **help_links** (optional): A list of dictionaries, each with keys 'text' and |
|
|||
130 | 'url'. These will be displayed in the help menu in the notebook UI. |
|
|||
131 |
|
118 | |||
132 | For example, the kernel.json file for IPython looks like this:: |
|
119 | For example, the kernel.json file for IPython looks like this:: | |
133 |
|
120 | |||
134 | { |
|
121 | { | |
135 | "argv": ["python3", "-c", "from IPython.kernel.zmq.kernelapp import main; main()", |
|
122 | "argv": ["python3", "-c", "from IPython.kernel.zmq.kernelapp import main; main()", | |
136 |
"-f", "{connection_file}"], |
|
123 | "-f", "{connection_file}"], | |
137 | "codemirror_mode": { |
|
|||
138 | "version": 3, |
|
|||
139 | "name": "ipython" |
|
|||
140 | }, |
|
|||
141 | "display_name": "IPython (Python 3)", |
|
124 | "display_name": "IPython (Python 3)", | |
142 | "language": "python" |
|
|||
143 | } |
|
125 | } | |
144 |
|
126 | |||
145 | To see the available kernel specs, run:: |
|
127 | To see the available kernel specs, run:: |
@@ -685,11 +685,33 b' Message type: ``kernel_info_reply``::' | |||||
685 | # included in IPython. |
|
685 | # included in IPython. | |
686 | 'language_version': 'X.Y.Z', |
|
686 | 'language_version': 'X.Y.Z', | |
687 |
|
687 | |||
|
688 | # Information about the language of code for the kernel | |||
|
689 | 'language_info': { | |||
|
690 | 'mimetype': str, | |||
|
691 | ||||
|
692 | # Pygments lexer, for highlighting | |||
|
693 | # Only needed if it differs from the top level 'language' field. | |||
|
694 | 'pygments_lexer': str, | |||
|
695 | ||||
|
696 | # Codemirror mode, for for highlighting in the notebook. | |||
|
697 | # Only needed if it differs from the top level 'language' field. | |||
|
698 | 'codemirror_mode': str or dict, | |||
|
699 | }, | |||
|
700 | ||||
688 | # A banner of information about the kernel, |
|
701 | # A banner of information about the kernel, | |
689 | # which may be desplayed in console environments. |
|
702 | # which may be desplayed in console environments. | |
690 | 'banner' : str, |
|
703 | 'banner' : str, | |
|
704 | ||||
|
705 | # Optional: A list of dictionaries, each with keys 'text' and 'url'. | |||
|
706 | # These will be displayed in the help menu in the notebook UI. | |||
|
707 | 'help_links': [ | |||
|
708 | {'text': str, 'url': str} | |||
|
709 | ], | |||
691 | } |
|
710 | } | |
692 |
|
711 | |||
|
712 | Refer to the lists of available `Pygments lexers <http://pygments.org/docs/lexers/>`_ | |||
|
713 | and `codemirror modes <http://codemirror.net/mode/index.html>`_ for those fields. | |||
|
714 | ||||
693 | .. versionchanged:: 5.0 |
|
715 | .. versionchanged:: 5.0 | |
694 |
|
716 | |||
695 | Versions changed from lists of integers to strings. |
|
717 | Versions changed from lists of integers to strings. | |
@@ -700,7 +722,8 b' Message type: ``kernel_info_reply``::' | |||||
700 |
|
722 | |||
701 | .. versionchanged:: 5.0 |
|
723 | .. versionchanged:: 5.0 | |
702 |
|
724 | |||
703 |
``implementation``, ``implementation_version``, |
|
725 | ``language_info``, ``implementation``, ``implementation_version``, ``banner`` | |
|
726 | and ``help_links`` keys are added. | |||
704 |
|
727 | |||
705 | .. _msging_shutdown: |
|
728 | .. _msging_shutdown: | |
706 |
|
729 |
@@ -34,6 +34,16 b' following methods and attributes:' | |||||
34 | interprets (e.g. Python). The 'banner' is displayed to the user in console |
|
34 | interprets (e.g. Python). The 'banner' is displayed to the user in console | |
35 | UIs before the first prompt. All of these values are strings. |
|
35 | UIs before the first prompt. All of these values are strings. | |
36 |
|
36 | |||
|
37 | .. attribute:: language_info | |||
|
38 | ||||
|
39 | Language information for :ref:`msging_kernel_info` replies, in a dictionary. | |||
|
40 | This should contain the key ``mimetype`` with the mimetype of code in the | |||
|
41 | target language (e.g. ``'text/x-python'``). It may also contain keys | |||
|
42 | ``codemirror_mode`` and ``pygments_lexer`` if they need to differ from | |||
|
43 | :attr:`language`. | |||
|
44 | ||||
|
45 | Other keys may be added to this later. | |||
|
46 | ||||
37 | .. method:: do_execute(code, silent, store_history=True, user_expressions=None, allow_stdin=False) |
|
47 | .. method:: do_execute(code, silent, store_history=True, user_expressions=None, allow_stdin=False) | |
38 |
|
48 | |||
39 | Execute user code. |
|
49 | Execute user code. | |
@@ -71,6 +81,7 b' Example' | |||||
71 | implementation_version = '1.0' |
|
81 | implementation_version = '1.0' | |
72 | language = 'no-op' |
|
82 | language = 'no-op' | |
73 | language_version = '0.1' |
|
83 | language_version = '0.1' | |
|
84 | language_info = {'mimetype': 'text/plain'} | |||
74 | banner = "Echo kernel - as useful as a parrot" |
|
85 | banner = "Echo kernel - as useful as a parrot" | |
75 |
|
86 | |||
76 | def do_execute(self, code, silent, store_history=True, user_expressions=None, |
|
87 | def do_execute(self, code, silent, store_history=True, user_expressions=None, | |
@@ -94,7 +105,6 b" Here's the Kernel spec ``kernel.json`` file for this::" | |||||
94 |
|
105 | |||
95 | {"argv":["python","-m","echokernel", "-f", "{connection_file}"], |
|
106 | {"argv":["python","-m","echokernel", "-f", "{connection_file}"], | |
96 | "display_name":"Echo", |
|
107 | "display_name":"Echo", | |
97 | "language":"no-op" |
|
|||
98 | } |
|
108 | } | |
99 |
|
109 | |||
100 |
|
110 |
General Comments 0
You need to be logged in to leave comments.
Login now