Skip to content

Revamp widget parameterization #14326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft

Conversation

amCap1712
Copy link
Contributor

Based on #13051 (comment), this PR explores a new way to parameterize widgets. Here is the what the proposed UI looks like:

image

The widget, language, component, code, and color options are available for all widgets. All the extra options per widget are defined in the extra_parameters attribute of the widget class and passed through JSON where jQuery reads it and renders it. Using the options, jQuery generates the urls dynamically, and updates the live preview and the code to embed the images.

Also, this is just a draft to start discussion.

Add a status badge to display the count of translated languages for
a project or component.
Revamp the widgets page. Use a card based layout for settings, preview and showing
the embed code. The current refactoring unifies the settings UI on the frontend
and generates the urls for various widgets on the frontend as well. Currently, this
serves no extra benefit but will prove to be helpful in future when we add additional
settings which are only applicable to specific widgets.
Copy link

@accesslint accesslint bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are accessibility issues in these changes.

widget.extra_parameters.forEach(param => {
let input;
if (param.type === 'number') {
input = $('<input/>', {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.


<div class="widget-card bg-white p-3">
<h4>Embed Code</h4>
<textarea id="embedCode" class="code-example form-control" rows="3" readonly>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.

Copy link

@accesslint accesslint bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are accessibility issues in these changes.

widget.extra_parameters.forEach((param) => {
let input;
if (param.type === "number") {
input = $("<input/>", {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.


<div class="widget-card bg-white p-3">
<h4>Embed Code</h4>
<textarea id="embedCode" class="code-example form-control" rows="3" readonly>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.

Copy link

codecov bot commented Mar 24, 2025

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
5019 3 5016 494
View the top 3 failed test(s) by shortest run time
weblate.trans.tests.test_widgets.WidgetsTest::test_view_widgets_lang
Stack Traces | 0.532s run time
self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/"...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
>                   current = current[bit]

.venv/lib/python3.13.../django/template/base.py:883: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/"...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]
key = 'image_src'

    def __getitem__(self, key):
        "Get a variable's value, starting at the current context and going upward"
        for d in reversed(self.dicts):
            if key in d:
                return d[key]
>       raise KeyError(key)
E       KeyError: 'image_src'

.venv/lib/python3.13.../django/template/context.py:85: KeyError

During handling of the above exception, another exception occurred:

self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/"...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
                    current = current[bit]
                    # ValueError/IndexError are for numpy.array lookup on
                    # numpy < 1.9 and 1.9+ respectively
                except (TypeError, AttributeError, KeyError, ValueError, IndexError):
                    try:  # attribute lookup
                        # Don't return class attributes if the class is the context:
>                       if isinstance(current, BaseContext) and getattr(
                            type(current), bit
                        ):
E                       AttributeError: type object 'RequestContext' has no attribute 'image_src'

.venv/lib/python3.13.../django/template/base.py:889: AttributeError

During handling of the above exception, another exception occurred:

self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/"...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
                    current = current[bit]
                    # ValueError/IndexError are for numpy.array lookup on
                    # numpy < 1.9 and 1.9+ respectively
                except (TypeError, AttributeError, KeyError, ValueError, IndexError):
                    try:  # attribute lookup
                        # Don't return class attributes if the class is the context:
                        if isinstance(current, BaseContext) and getattr(
                            type(current), bit
                        ):
                            raise AttributeError
                        current = getattr(current, bit)
                    except (TypeError, AttributeError):
                        # Reraise if the exception was raised by a @property
                        if not isinstance(current, BaseContext) and bit in dir(current):
                            raise
                        try:  # list-index lookup
>                           current = current[int(bit)]
E                           ValueError: invalid literal for int() with base 10: 'image_src'

.venv/lib/python3.13.../django/template/base.py:899: ValueError

During handling of the above exception, another exception occurred:

self = <FilterExpression 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/"...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]
ignore_failures = False

    def resolve(self, context, ignore_failures=False):
        if self.is_var:
            try:
>               obj = self.var.resolve(context)

.venv/lib/python3.13.../django/template/base.py:718: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.13.../django/template/base.py:850: in resolve
    value = self._resolve_lookup(context)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/"...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
                    current = current[bit]
                    # ValueError/IndexError are for numpy.array lookup on
                    # numpy < 1.9 and 1.9+ respectively
                except (TypeError, AttributeError, KeyError, ValueError, IndexError):
                    try:  # attribute lookup
                        # Don't return class attributes if the class is the context:
                        if isinstance(current, BaseContext) and getattr(
                            type(current), bit
                        ):
                            raise AttributeError
                        current = getattr(current, bit)
                    except (TypeError, AttributeError):
                        # Reraise if the exception was raised by a @property
                        if not isinstance(current, BaseContext) and bit in dir(current):
                            raise
                        try:  # list-index lookup
                            current = current[int(bit)]
                        except (
                            IndexError,  # list index out of range
                            ValueError,  # invalid literal for int()
                            KeyError,  # current is a dict without `int(bit)` key
                            TypeError,
                        ):  # unsubscriptable object
>                           raise VariableDoesNotExist(
                                "Failed lookup for key [%s] in %r",
                                (bit, current),
                            )  # missing attribute
E                           django.template.base.VariableDoesNotExist: Failed lookup for key [image_src] in [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example..../test/-/cs/" id="engage-link">http://example..../test/-/cs/</a>', 'widget_list': [{'name': 'svg', 'verbose': 'SVG status badge'}, {'name': 'status', 'verbose': 'PNG status badge'}, {'name': 'multi', 'verbose': 'Vertical language bar chart'}, {'name': 'horizontal', 'verbose': 'Horizontal language bar chart'}, {'name': 'language', 'verbose': 'Language count badge'}, {'name': '287x66', 'verbose': 'Big status badge'}, {'name': '88x31', 'verbose': 'Small status badge'}, {'name': 'open', 'verbose': 'Panel'}], 'object': <weblate.utils.stats.ProjectLanguage object at 0x7f3c843eb2a0>, 'project': <Project: Test>, 'form': <EngageForm bound=True, valid=True, fields=(lang;component)>, 'widgets_json': '{"widget_base_url": "http://example.com/widget/test", "engage_base_url": "http://example.com/engage/test/", "language": "cs", "component": null, "components": [{"id": 1, "slug": "test"}], "translation_status": "Translation status", "widgets": {"svg": {"name": "svg", "extension": "svg", "colors": ["badge"], "extra_parameters": []}, "status": {"name": "status", "extension": "png", "colors": ["badge"], "extra_parameters": []}, "multi": {"name": "multi", "extension": "svg", "colors": ["auto", "red", "green", "blue"], "extra_parameters": []}, "horizontal": {"name": "horizontal", "extension": "svg", "colors": ["auto", "red", "green", "blue"], "extra_parameters": []}, "language": {"name": "language", "extension": "svg", "colors": ["badge"], "extra_parameters": [{"name": "threshold", "label": "Threshold", "type": "number", "default": 0, "min": 0, "max": 100, "step": 1}]}, "287x66": {"name": "287x66", "extension": "png", "colors": ["grey", "white", "black"], "extra_parameters": []}, "88x31": {"name": "88x31", "extension": "png", "colors": ["grey", "white", "black"], "extra_parameters": []}, "open": {"name": "open", "extension": "png", "colors": ["graph"], "extra_parameters": []}}}', 'description': 'Test is being translated into 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

.venv/lib/python3.13.../django/template/base.py:906: VariableDoesNotExist

During handling of the above exception, another exception occurred:

self = <weblate.trans.tests.test_widgets.WidgetsTest testMethod=test_view_widgets_lang>

    def test_view_widgets_lang(self) -> None:
>       response = self.client.get(
            reverse("widgets", kwargs={"path": self.project.get_url_path()}),
            {"lang": "cs"},
        )

.../trans/tests/test_widgets.py:31: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.13.../django/test/client.py:1129: in get
    response = super().get(
.venv/lib/python3.13.../django/test/client.py:479: in get
    return self.generic(
.venv/lib/python3.13.../django/test/client.py:676: in generic
    return self.request(**r)
.venv/lib/python3.13.../django/test/client.py:1087: in request
    response = self.handler(environ)
.venv/lib/python3.13.../django/test/client.py:186: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/base.py:140: in get_response
    response = self._middleware_chain(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/middleware.py:119: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/middleware.py:104: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../site-packages/corsheaders/middleware.py:56: in __call__
    result = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/accounts/middleware.py:70: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../site-packages/django_otp/middleware.py:35: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../site-packages/social_django/middleware.py:28: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/accounts/middleware.py:154: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/api/middleware.py:27: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/middleware.py:462: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/wladmin/middleware.py:66: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../core/handlers/base.py:197: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
.../trans/views/widgets.py:85: in widgets
    return render(
weblate/trans/util.py:261: in render
    return django.shortcuts.render(
.venv/lib/python3.13.../site-packages/django/shortcuts.py:25: in render
    content = loader.render_to_string(template_name, context, request, using=using)
.venv/lib/python3.13.../django/template/loader.py:62: in render_to_string
    return template.render(context, request)
.venv/lib/python3.13.../template/backends/django.py:107: in render
    return self.template.render(context)
.venv/lib/python3.13.../django/template/base.py:171: in render
    return self._render(context)
.venv/lib/python3.13.../django/test/utils.py:114: in instrumented_test_render
    return self.nodelist.render(context)
.venv/lib/python3.13.../django/template/base.py:1008: in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
.venv/lib/python3.13.../django/template/base.py:969: in render_annotated
    return self.render(context)
.venv/lib/python3.13.../django/template/loader_tags.py:159: in render
    return compiled_parent._render(context)
.venv/lib/python3.13.../django/test/utils.py:114: in instrumented_test_render
    return self.nodelist.render(context)
.venv/lib/python3.13.../django/template/base.py:1008: in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
.venv/lib/python3.13.../django/template/base.py:969: in render_annotated
    return self.render(context)
.venv/lib/python3.13.../django/template/loader_tags.py:65: in render
    result = block.nodelist.render(context)
.venv/lib/python3.13.../django/template/base.py:1008: in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
.venv/lib/python3.13.../django/template/base.py:969: in render_annotated
    return self.render(context)
.venv/lib/python3.13.../django/template/base.py:1067: in render
    output = self.filter_expression.resolve(context)
.venv/lib/python3.13.../django/template/base.py:726: in resolve
    return string_if_invalid % self.var
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pytest_django.plugin._fail_for_invalid_template_variable.<locals>.InvalidVarException object at 0x7f3c86b16510>
var = <Variable: 'image_src'>

    def __mod__(self, var: str) -> str:
        origin = self._get_origin()
        if origin:
            msg = f"Undefined template variable '{var}' in '{origin}'"
        else:
            msg = f"Undefined template variable '{var}'"
        if self.fail:
>           pytest.fail(msg)
E           Failed: Undefined template variable 'image_src' in '.../weblate/templates/widgets.html'

.venv/lib/python3.13.../site-packages/pytest_django/plugin.py:722: Failed
weblate.trans.tests.test_widgets.WidgetsTest::test_view_widgets
Stack Traces | 0.676s run time
self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
>                   current = current[bit]

.venv/lib/python3.13.../django/template/base.py:883: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]
key = 'image_src'

    def __getitem__(self, key):
        "Get a variable's value, starting at the current context and going upward"
        for d in reversed(self.dicts):
            if key in d:
                return d[key]
>       raise KeyError(key)
E       KeyError: 'image_src'

.venv/lib/python3.13.../django/template/context.py:85: KeyError

During handling of the above exception, another exception occurred:

self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
                    current = current[bit]
                    # ValueError/IndexError are for numpy.array lookup on
                    # numpy < 1.9 and 1.9+ respectively
                except (TypeError, AttributeError, KeyError, ValueError, IndexError):
                    try:  # attribute lookup
                        # Don't return class attributes if the class is the context:
>                       if isinstance(current, BaseContext) and getattr(
                            type(current), bit
                        ):
E                       AttributeError: type object 'RequestContext' has no attribute 'image_src'

.venv/lib/python3.13.../django/template/base.py:889: AttributeError

During handling of the above exception, another exception occurred:

self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
                    current = current[bit]
                    # ValueError/IndexError are for numpy.array lookup on
                    # numpy < 1.9 and 1.9+ respectively
                except (TypeError, AttributeError, KeyError, ValueError, IndexError):
                    try:  # attribute lookup
                        # Don't return class attributes if the class is the context:
                        if isinstance(current, BaseContext) and getattr(
                            type(current), bit
                        ):
                            raise AttributeError
                        current = getattr(current, bit)
                    except (TypeError, AttributeError):
                        # Reraise if the exception was raised by a @property
                        if not isinstance(current, BaseContext) and bit in dir(current):
                            raise
                        try:  # list-index lookup
>                           current = current[int(bit)]
E                           ValueError: invalid literal for int() with base 10: 'image_src'

.venv/lib/python3.13.../django/template/base.py:899: ValueError

During handling of the above exception, another exception occurred:

self = <FilterExpression 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]
ignore_failures = False

    def resolve(self, context, ignore_failures=False):
        if self.is_var:
            try:
>               obj = self.var.resolve(context)

.venv/lib/python3.13.../django/template/base.py:718: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.13.../django/template/base.py:850: in resolve
    value = self._resolve_lookup(context)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Variable: 'image_src'>
context = [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="...nto 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

    def _resolve_lookup(self, context):
        """
        Perform resolution of a real variable (i.e. not a literal) against the
        given context.
    
        As indicated by the method's name, this method is an implementation
        detail and shouldn't be called by external code. Use Variable.resolve()
        instead.
        """
        current = context
        try:  # catch-all for silent variable failures
            for bit in self.lookups:
                try:  # dictionary lookup
                    current = current[bit]
                    # ValueError/IndexError are for numpy.array lookup on
                    # numpy < 1.9 and 1.9+ respectively
                except (TypeError, AttributeError, KeyError, ValueError, IndexError):
                    try:  # attribute lookup
                        # Don't return class attributes if the class is the context:
                        if isinstance(current, BaseContext) and getattr(
                            type(current), bit
                        ):
                            raise AttributeError
                        current = getattr(current, bit)
                    except (TypeError, AttributeError):
                        # Reraise if the exception was raised by a @property
                        if not isinstance(current, BaseContext) and bit in dir(current):
                            raise
                        try:  # list-index lookup
                            current = current[int(bit)]
                        except (
                            IndexError,  # list index out of range
                            ValueError,  # invalid literal for int()
                            KeyError,  # current is a dict without `int(bit)` key
                            TypeError,
                        ):  # unsubscriptable object
>                           raise VariableDoesNotExist(
                                "Failed lookup for key [%s] in %r",
                                (bit, current),
                            )  # missing attribute
E                           django.template.base.VariableDoesNotExist: Failed lookup for key [image_src] in [{'True': True, 'False': False, 'None': None}, {}, {}, {'engage_link': '<a href="http://example.com/engage/test/" id="engage-link">http://example.com/engage/test/</a>', 'widget_list': [{'name': 'svg', 'verbose': 'SVG status badge'}, {'name': 'status', 'verbose': 'PNG status badge'}, {'name': 'multi', 'verbose': 'Vertical language bar chart'}, {'name': 'horizontal', 'verbose': 'Horizontal language bar chart'}, {'name': 'language', 'verbose': 'Language count badge'}, {'name': '287x66', 'verbose': 'Big status badge'}, {'name': '88x31', 'verbose': 'Small status badge'}, {'name': 'open', 'verbose': 'Panel'}], 'object': <Project: Test>, 'project': <Project: Test>, 'form': <EngageForm bound=True, valid=True, fields=(lang;component)>, 'widgets_json': '{"widget_base_url": "http://example.com/widget/test", "engage_base_url": "http://example.com/engage/test/", "language": null, "component": null, "components": [{"id": 1, "slug": "test"}], "translation_status": "Translation status", "widgets": {"svg": {"name": "svg", "extension": "svg", "colors": ["badge"], "extra_parameters": []}, "status": {"name": "status", "extension": "png", "colors": ["badge"], "extra_parameters": []}, "multi": {"name": "multi", "extension": "svg", "colors": ["auto", "red", "green", "blue"], "extra_parameters": []}, "horizontal": {"name": "horizontal", "extension": "svg", "colors": ["auto", "red", "green", "blue"], "extra_parameters": []}, "language": {"name": "language", "extension": "svg", "colors": ["badge"], "extra_parameters": [{"name": "threshold", "label": "Threshold", "type": "number", "default": 0, "min": 0, "max": 100, "step": 1}]}, "287x66": {"name": "287x66", "extension": "png", "colors": ["grey", "white", "black"], "extra_parameters": []}, "88x31": {"name": "88x31", "extension": "png", "colors": ["grey", "white", "black"], "extra_parameters": []}, "open": {"name": "open", "extension": "png", "colors": ["graph"], "extra_parameters": []}}}', 'description': 'Test is being translated into 4 languages using Weblate. Join the translation or start translating your own project.', 'user_can_manage': False}]

.venv/lib/python3.13.../django/template/base.py:906: VariableDoesNotExist

During handling of the above exception, another exception occurred:

self = <weblate.trans.tests.test_widgets.WidgetsTest testMethod=test_view_widgets>

    def test_view_widgets(self) -> None:
>       response = self.client.get(
            reverse("widgets", kwargs={"path": self.project.get_url_path()})
        )

.../trans/tests/test_widgets.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.13.../django/test/client.py:1129: in get
    response = super().get(
.venv/lib/python3.13.../django/test/client.py:479: in get
    return self.generic(
.venv/lib/python3.13.../django/test/client.py:676: in generic
    return self.request(**r)
.venv/lib/python3.13.../django/test/client.py:1087: in request
    response = self.handler(environ)
.venv/lib/python3.13.../django/test/client.py:186: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/base.py:140: in get_response
    response = self._middleware_chain(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/middleware.py:119: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/middleware.py:104: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../site-packages/corsheaders/middleware.py:56: in __call__
    result = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/accounts/middleware.py:70: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../site-packages/django_otp/middleware.py:35: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../django/utils/deprecation.py:129: in __call__
    response = response or self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../site-packages/social_django/middleware.py:28: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/accounts/middleware.py:154: in __call__
    return self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/api/middleware.py:27: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/middleware.py:462: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
weblate/wladmin/middleware.py:66: in __call__
    response = self.get_response(request)
.venv/lib/python3.13.../core/handlers/exception.py:55: in inner
    response = get_response(request)
.venv/lib/python3.13.../core/handlers/base.py:197: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
.../trans/views/widgets.py:85: in widgets
    return render(
weblate/trans/util.py:261: in render
    return django.shortcuts.render(
.venv/lib/python3.13.../site-packages/django/shortcuts.py:25: in render
    content = loader.render_to_string(template_name, context, request, using=using)
.venv/lib/python3.13.../django/template/loader.py:62: in render_to_string
    return template.render(context, request)
.venv/lib/python3.13.../template/backends/django.py:107: in render
    return self.template.render(context)
.venv/lib/python3.13.../django/template/base.py:171: in render
    return self._render(context)
.venv/lib/python3.13.../django/test/utils.py:114: in instrumented_test_render
    return self.nodelist.render(context)
.venv/lib/python3.13.../django/template/base.py:1008: in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
.venv/lib/python3.13.../django/template/base.py:969: in render_annotated
    return self.render(context)
.venv/lib/python3.13.../django/template/loader_tags.py:159: in render
    return compiled_parent._render(context)
.venv/lib/python3.13.../django/test/utils.py:114: in instrumented_test_render
    return self.nodelist.render(context)
.venv/lib/python3.13.../django/template/base.py:1008: in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
.venv/lib/python3.13.../django/template/base.py:969: in render_annotated
    return self.render(context)
.venv/lib/python3.13.../django/template/loader_tags.py:65: in render
    result = block.nodelist.render(context)
.venv/lib/python3.13.../django/template/base.py:1008: in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
.venv/lib/python3.13.../django/template/base.py:969: in render_annotated
    return self.render(context)
.venv/lib/python3.13.../django/template/base.py:1067: in render
    output = self.filter_expression.resolve(context)
.venv/lib/python3.13.../django/template/base.py:726: in resolve
    return string_if_invalid % self.var
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pytest_django.plugin._fail_for_invalid_template_variable.<locals>.InvalidVarException object at 0x7f3c86b16510>
var = <Variable: 'image_src'>

    def __mod__(self, var: str) -> str:
        origin = self._get_origin()
        if origin:
            msg = f"Undefined template variable '{var}' in '{origin}'"
        else:
            msg = f"Undefined template variable '{var}'"
        if self.fail:
>           pytest.fail(msg)
E           Failed: Undefined template variable 'image_src' in '.../weblate/templates/widgets.html'

.venv/lib/python3.13.../site-packages/pytest_django/plugin.py:722: Failed
weblate.trans.tests.test_selenium.SeleniumTests::test_weblate
Stack Traces | 29.2s run time
self = <weblate.trans.tests.test_selenium.SeleniumTests testMethod=test_weblate>

    def test_weblate(self) -> None:  # noqa: PLR0915
        user = self.open_admin()
        language_regex = "^(cs|he|hu)$"
    
        # Add project
        with self.wait_for_page_load():
            self.click("Projects")
        with self.wait_for_page_load():
            self.click(self.driver.find_element(By.CLASS_NAME, "addlink"))
        self.driver.find_element(By.ID, "id_name").send_keys("WeblateOrg")
        Select(self.driver.find_element(By.ID, "id_access_control")).select_by_value(
            "1"
        )
        self.driver.find_element(By.ID, "id_web").send_keys("https://weblate.org/")
        self.driver.find_element(By.ID, "id_instructions").send_keys(
            "https://weblate.org/contribute/"
        )
        self.screenshot("add-project.png")
        with self.wait_for_page_load():
            self.driver.find_element(By.ID, "id_name").submit()
    
        # Add bilingual component
        with self.wait_for_page_load():
            self.click("Home")
        with self.wait_for_page_load():
            self.click("Components")
        with self.wait_for_page_load():
            self.click(self.driver.find_element(By.CLASS_NAME, "addlink"))
    
        self.driver.find_element(By.ID, "id_name").send_keys("Language names")
        Select(self.driver.find_element(By.ID, "id_project")).select_by_visible_text(
            "WeblateOrg"
        )
        self.driver.find_element(By.ID, "id_repo").send_keys(
            "https://github.com/WeblateOrg/demo.git"
        )
        self.driver.find_element(By.ID, "id_repoweb").send_keys(
            "https://github..../WeblateOrg/demo/blob/{{branch}}/{{filename}}#L{{line}}"
        )
        self.driver.find_element(By.ID, "id_filemask").send_keys(
            "weblate/langdata/locale/*/LC_MESSAGES/django.po"
        )
        self.driver.find_element(By.ID, "id_new_base").send_keys(
            ".../langdata/locale/django.pot"
        )
        Select(self.driver.find_element(By.ID, "id_file_format")).select_by_value("po")
        Select(self.driver.find_element(By.ID, "id_license")).select_by_value(
            "GPL-3.0-or-later"
        )
        element = self.driver.find_element(By.ID, "id_language_regex")
        element.clear()
        element.send_keys(language_regex)
        self.screenshot("add-component.png")
        # This takes long
        with self.wait_for_page_load(timeout=1200):
            self.driver.find_element(By.ID, "id_name").submit()
        with self.wait_for_page_load():
            self.click("Language names")
    
        # Add monolingual component
        with self.wait_for_page_load():
            self.click("Components")
        with self.wait_for_page_load():
            self.click(self.driver.find_element(By.CLASS_NAME, "addlink"))
        self.driver.find_element(By.ID, "id_name").send_keys("Android")
        Select(self.driver.find_element(By.ID, "id_project")).select_by_visible_text(
            "WeblateOrg"
        )
        self.driver.find_element(By.ID, "id_repo").send_keys(
            "weblate:.../weblateorg/language-names"
        )
        self.driver.find_element(By.ID, "id_filemask").send_keys(
            ".../main/res/values-*/strings.xml"
        )
        self.driver.find_element(By.ID, "id_template").send_keys(
            ".../main/res/values/strings.xml"
        )
        Select(self.driver.find_element(By.ID, "id_file_format")).select_by_value(
            "aresource"
        )
        Select(self.driver.find_element(By.ID, "id_license")).select_by_value("MIT")
        self.screenshot("add-component-mono.png")
        # This takes long
        with self.wait_for_page_load(timeout=1200):
            self.driver.find_element(By.ID, "id_name").submit()
        with self.wait_for_page_load():
            self.click("Android")
    
        # Load Weblate project page
        self.view_site()
        self.click(htmlid="projects-menu")
        with self.wait_for_page_load():
            self.click("Browse all projects")
        with self.wait_for_page_load():
            self.click("WeblateOrg")
    
        self.screenshot("project-overview.png")
    
        # User management
        self.click("Manage")
        with self.wait_for_page_load():
            self.click("Users")
        element = self.driver.find_element(By.ID, "id_user")
        element.send_keys("testuser")
        with self.wait_for_page_load():
            element.submit()
        with self.wait_for_page_load():
            self.click("Access control")
        self.screenshot("manage-users.png")
        # Automatic suggestions
        self.click(htmlid="projects-menu")
        with self.wait_for_page_load():
            self.click("WeblateOrg")
        self.click("Manage")
        with self.wait_for_page_load():
            self.click("Automatic suggestions")
        self.screenshot("project-machinery.png")
        # Access control settings
        self.click(htmlid="projects-menu")
        with self.wait_for_page_load():
            self.click("WeblateOrg")
        self.click("Manage")
        with self.wait_for_page_load():
            self.click("Settings")
        self.click("Access")
        self.screenshot("project-access.png")
        self.click("Workflow")
        self.screenshot("project-workflow.png")
        # The project is now watched
        self.click(htmlid="projects-menu")
        with self.wait_for_page_load():
            self.click("WeblateOrg")
    
        # Engage page
        self.click("Share")
        with self.wait_for_page_load():
            self.click("Status widgets")
        self.screenshot("promote.png")
        with self.wait_for_page_load():
>           self.click(htmlid="engage-link")

.../trans/tests/test_selenium.py:804: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../trans/tests/test_selenium.py:216: in click
    element = self.driver.find_element(By.ID, htmlid)
.venv/lib/python3.13.../webdriver/remote/webdriver.py:898: in find_element
    return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
.venv/lib/python3.13.../webdriver/remote/webdriver.py:429: in execute
    self.error_handler.check_response(response)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f03f291f620>
response = {'status': 404, 'value': '{"value":{"error":"no such element","message":"no such element: Unable to locate element: {\...\\n#17 0x5603d6f30896 \\u003Cunknown>\\n#18 0x7f671249caa4 \\u003Cunknown>\\n#19 0x7f6712529c3c \\u003Cunknown>\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -> None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "<anonymous>")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "<anonymous>")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
>       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="engage-link"]"}
E         (Session info: chrome=134.0.6998.88); For documentation on this error, please visit: https://www.selenium..../webdriver/troubleshooting/errors#no-such-element-exception
E       Stacktrace:
E       #0 0x5603d6f31a1a <unknown>
E       #1 0x5603d69e9390 <unknown>
E       #2 0x5603d6a3ac85 <unknown>
E       #3 0x5603d6a3aeb1 <unknown>
E       #4 0x5603d6a89d64 <unknown>
E       #5 0x5603d6a60bfd <unknown>
E       #6 0x5603d6a8707b <unknown>
E       #7 0x5603d6a609a3 <unknown>
E       #8 0x5603d6a2c60e <unknown>
E       #9 0x5603d6a2ddd1 <unknown>
E       #10 0x5603d6ef7ddb <unknown>
E       #11 0x5603d6efbcbc <unknown>
E       #12 0x5603d6edf392 <unknown>
E       #13 0x5603d6efc834 <unknown>
E       #14 0x5603d6ec31ef <unknown>
E       #15 0x5603d6f20038 <unknown>
E       #16 0x5603d6f20216 <unknown>
E       #17 0x5603d6f30896 <unknown>
E       #18 0x7f671249caa4 <unknown>
E       #19 0x7f6712529c3c <unknown>

.venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: NoSuchElementException

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link
Member

@nijel nijel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this goes into the correct direction. I'm not sure about the frontend code, I will add @meel-hd to review that.

"social:begin",
"djangosaml2idp:saml_login_process",
}
INLINE_PATHS = {"social:begin", "djangosaml2idp:saml_login_process", "widgets"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid this, these are third-party apps where we can't avoid that, but our code should not use inline javascript.

@@ -1,4 +1,4 @@
# Copyright © Michal Čihař <michal@weblate.org>
# Copyright Michal Čihař <michal@weblate.org>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like unintended change.

</div>

{% endblock %}

{% block extra_script %}
<script type="application/json" id="widgets-data">{{ widgets_json|safe }}</script>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think safe should be used here. The JSON can still contain markup and that would break. Also passing this as data attribute would be better approach as that would not need inline scripts in CSP and you would have no issues with escaping.

@nijel nijel requested a review from meel-hd March 25, 2025 13:32
Comment on lines +2363 to +2364
border: 1px solid var(--altcha-color-border);
border-radius: var(--altcha-border-radius);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variables are specific to a 3rd party lib, use the more general alternatives defined in variables.css

box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 15px;
background-color: #fff;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this in dark-styles.css to be suitable for dark theme

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants