Add an Avatar Service Registry to Djblets

Review Request #7844 — Created Jan. 6, 2016 and submitted

Information

Djblets
release-0.10.x
5146fb6...

Reviewers

The avatar services registry is a new object for managing avatar
services, which are defined to provide multiple ways of displaying
users' avatars. Currently, the only supported service is the Gravatar
(https://gravatar.com) service.

Avatars support the srcset attribute for <img> tags. In browsers
where it is not supported, a polyfill is used.

  • Ran unit tests
  • Used this with an upcoming patch in Review Board.
  • Tested the srcset polyfill in IE11 with Review Board.
Description From Last Updated

Col: 1 W391 blank line at end of file

reviewbotreviewbot

Col: 21 E128 continuation line under-indented for visual indent

reviewbotreviewbot

Col: 21 E128 continuation line under-indented for visual indent

reviewbotreviewbot

Col: 21 E128 continuation line under-indented for visual indent

reviewbotreviewbot

Col: 1 E303 too many blank lines (3)

reviewbotreviewbot

The suspense is killing me.

daviddavid

Blank line after this.

daviddavid

Blank line after this.

daviddavid

Missing the "Yields" label.

daviddavid

Missing "Raises"

daviddavid

Should we include 3x for future proofing?

daviddavid

This is wordy. Can we drop the first "services"? Maybe just "avatars_enabled_services" and "avatars_default_service"?

chipx86chipx86

The set shouldn't be there.

chipx86chipx86

Given the complexity of this and the getter (more this one), and the note about having to call save manually, …

chipx86chipx86

I feel that we should be using subclasses of this. Much of the implementation of registries should be an internal …

chipx86chipx86

Needs the full path. Same with others below.

chipx86chipx86

There's an extra "and the".

chipx86chipx86

Instead of passing a default, we should fall back on what the app may have specified as the default when …

chipx86chipx86

No blank line.

chipx86chipx86

If we make use of siteconfig defaults, this information can be encoded there, rather than being hard-coded here.

chipx86chipx86

"human-readable"

chipx86chipx86

This sounds like the template will be rendering an image, but it's the HTML tags for an image.

chipx86chipx86

Extra "the"

chipx86chipx86

Alignment issue.

chipx86chipx86

A more efficient way of doing this would be: try: urls = request._avatar_cache[key] except KeyError: urls = self.get_blah(...) request._avatar_cache[key] = …

chipx86chipx86

Alignment issue.

chipx86chipx86

Let's change this to: raise NotImplementedError( '%r must implement get_avatar_urls_uncached().' % self.__class__ ) That way, when there are lots spewing …

chipx86chipx86

80 is the default for gravatars, but we care more about the caller. The caller should expect consistency in sizes, …

chipx86chipx86

We could support all ##x image sizes by doing: <img {% for key, url in urls.items %}{% if key|startswith:"@" %}{% …

chipx86chipx86

We should probably raise a ValueError if fmt is empty, instead of asserting, and specify the class and error name, …

chipx86chipx86

'mark_safe' imported but unused

reviewbotreviewbot

Description on the next line.

chipx86chipx86

The else is implied, so you can remove it. The first part is a guard, and doesn't have to be …

chipx86chipx86

You can remove the blank line here.

chipx86chipx86

This is already saved below. Also, are there cases where we don't need to save?

chipx86chipx86

Thinking this should be avatar_id. Some of our older stuff uses id (which is a reserved word), and most of …

chipx86chipx86

This is going to be a SafeText.

chipx86chipx86

Blank line not needed.

chipx86chipx86

Having a hard time parsing this. Maybe "The keyword arguments passed to the error-specific formatting string."

chipx86chipx86

No need for None.

chipx86chipx86

Let's include the class name in this (using __class__).

chipx86chipx86

No blank line.

chipx86chipx86

We shouldn't assume anything about the safeness of the URLs. If the URL has a & in it, then it'll …

chipx86chipx86
reviewbot
  1. Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
    
    Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
  2. djblets/avatars/errors.py (Diff revision 1)
     
     
    Col: 1
     W391 blank line at end of file
    
  3. djblets/avatars/tests.py (Diff revision 1)
     
     
    Col: 21
     E128 continuation line under-indented for visual indent
    
  4. djblets/avatars/tests.py (Diff revision 1)
     
     
    Col: 21
     E128 continuation line under-indented for visual indent
    
  5. djblets/avatars/tests.py (Diff revision 1)
     
     
    Col: 21
     E128 continuation line under-indented for visual indent
    
  6. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
  2. djblets/avatars/tests.py (Diff revision 2)
     
     
    Col: 1
     E303 too many blank lines (3)
    
  3. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
  2. 
      
david
  1. 
      
  2. djblets/avatars/errors.py (Diff revision 3)
     
     

    The suspense is killing me.

  3. djblets/avatars/registry.py (Diff revision 3)
     
     

    Blank line after this.

  4. djblets/avatars/registry.py (Diff revision 3)
     
     

    Blank line after this.

  5. djblets/avatars/registry.py (Diff revision 3)
     
     

    Missing the "Yields" label.

  6. djblets/avatars/registry.py (Diff revision 3)
     
     

    Missing "Raises"

  7. djblets/avatars/services/base.py (Diff revision 3)
     
     

    Should we include 3x for future proofing?

    1. If we do, we should say it may contain a 3x key. I suppose I should update the JS as well to support 3x avatars.

  8. 
      
brennie
reviewbot
  1. Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
    
    Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
  2. 
      
chipx86
  1. 
      
  2. djblets/avatars/registry.py (Diff revision 4)
     
     

    This is wordy. Can we drop the first "services"? Maybe just "avatars_enabled_services" and "avatars_default_service"?

  3. djblets/avatars/registry.py (Diff revision 4)
     
     
     
     

    The set shouldn't be there.

  4. djblets/avatars/registry.py (Diff revision 4)
     
     
     

    Given the complexity of this and the getter (more this one), and the note about having to call save manually, and how we have similar functions that have save as a built-in flag, I think maybe we should just use standard methods for this.

    Properties are great when it's a trivial thing, like taking a value and stuffing it in some nested object, or normalizing the value. A caller expects that an assignment like that will basically just work. This one is a lot more complicated, with the possibility of other exceptions being raised and the other requirements placed on the caller.

  5. djblets/avatars/registry.py (Diff revision 4)
     
     

    I feel that we should be using subclasses of this. Much of the implementation of registries should be an internal detail, and not something that the caller should have to know much about. Right now, as a caller, I have to learn not just about the avatar service, but I also have to dive into the registry methods.

    What about extending registries to have a class pointing to the exceptions that should be used? Then there could be an AvatarNotFoundError that's raised, subclassing ItemLookupError, when raised either by this subclass or by the base class.

    Such an error would have a lot more meaning to a user, and to anyone reading or contributing to the code.

  6. djblets/avatars/registry.py (Diff revision 4)
     
     

    Needs the full path. Same with others below.

  7. djblets/avatars/registry.py (Diff revision 4)
     
     

    There's an extra "and the".

  8. djblets/avatars/registry.py (Diff revision 4)
     
     

    Instead of passing a default, we should fall back on what the app may have specified as the default when registering settings. That way, we get that value even if not in the database.

  9. djblets/avatars/registry.py (Diff revision 4)
     
     
     
     

    No blank line.

  10. djblets/avatars/registry.py (Diff revision 4)
     
     
     
     
     
     
     

    If we make use of siteconfig defaults, this information can be encoded there, rather than being hard-coded here.

    1. It ends up being hardcoded somewhere. I'd rather it be hardcoded in the get_defaults method because thats where registry defaults come from elsewhere.

  11. djblets/avatars/services/base.py (Diff revision 4)
     
     

    "human-readable"

  12. djblets/avatars/services/base.py (Diff revision 4)
     
     

    This sounds like the template will be rendering an image, but it's the HTML tags for an image.

  13. djblets/avatars/services/base.py (Diff revision 4)
     
     

    Extra "the"

  14. djblets/avatars/services/base.py (Diff revision 4)
     
     
     

    Alignment issue.

  15. djblets/avatars/services/base.py (Diff revision 4)
     
     
     
     
     
     

    A more efficient way of doing this would be:

    try:
        urls = request._avatar_cache[key]
    except KeyError:
        urls = self.get_blah(...)
        request._avatar_cache[key] = urls
    
  16. djblets/avatars/services/base.py (Diff revision 4)
     
     
     

    Alignment issue.

  17. djblets/avatars/services/base.py (Diff revision 4)
     
     

    Let's change this to:

    raise NotImplementedError(
        '%r must implement get_avatar_urls_uncached().'
        % self.__class__
    )
    

    That way, when there are lots spewing errors, it's very clear what class is missing this definition.

    (Also parens to be clear it's a method.)

  18. djblets/avatars/services/gravatar.py (Diff revision 4)
     
     

    80 is the default for gravatars, but we care more about the caller. The caller should expect consistency in sizes, regardless of what the defaults are in the backend if a size is unspecified.

    Realistically, this value will almost surely never be used, because get_avatar_urls() is going to pass its own value.

    I'd suggest having get_avatar_urls_uncached, both here and in AvatarService, simply not take a default value for size.

  19. We could support all ##x image sizes by doing:

    <img {% for key, url in urls.items %}{% if key|startswith:"@" %}{% if key == "@1x" %}src{% else %}{{key}}{% endif %}="{{url}}"{% endif %} />
    

    However, <img srcset="..."/> and <picture/> are becoming things. These are neat because not only do they let you do "2x", "3x", etc., but also "512w" (for a 512 image width), which we could then allow in the list of URLs.

    I think we could probably start trying to use this now (would need some preliminary testing), whether this change or another, and bonus points: We could just find a polyfill for it instead of writing and requiring use of $.retinaAvatar.

    This could look like:

    <img src="{{urls.1x}}" srcset="{% for size, url in urls.items %}{% spaceless %}
        {{url}}{% if size != "1x" %} {{size}}{% endif %}{% if not forloop.last %}, {% endif %}
    {% endspaceless %}{% endfor %}" />
    

    (I'm just leaving off the other attributes from these examples, but we'd obviously still want them.)

    1. I'm going to update this to use srcset :)

  20. djblets/registries/registry.py (Diff revision 4)
     
     
     
     

    We should probably raise a ValueError if fmt is empty, instead of asserting, and specify the class and error name, so it's more clear what's going on if this is hit.

  21. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/configforms/mixins.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/configforms/registry.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
        djblets/configforms/tests.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/configforms/mixins.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/configforms/registry.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
        djblets/configforms/tests.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
  2. 
      
david
  1. This looks good to me now. May want to get Christian to give it another look too, since it's pretty big.

  2. 
      
brennie
reviewbot
  1. Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
    
    Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/services/__init__.py
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
    
    
  2. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        docs/djblets/guides/index.rst
        djblets/avatars/services/__init__.py
        docs/djblets/guides/avatars/writing-avatar-services.rst
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
        docs/djblets/guides/avatars/index.rst
        docs/djblets/guides/registries/writing-registries.rst
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        docs/djblets/guides/index.rst
        djblets/avatars/services/__init__.py
        docs/djblets/guides/avatars/writing-avatar-services.rst
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
        docs/djblets/guides/avatars/index.rst
        docs/djblets/guides/registries/writing-registries.rst
    
    
  2. djblets/avatars/services/gravatar.py (Diff revision 7)
     
     
     'mark_safe' imported but unused
    
  3. 
      
brennie
reviewbot
  1. Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        docs/djblets/guides/index.rst
        djblets/avatars/services/__init__.py
        docs/djblets/guides/avatars/writing-avatar-services.rst
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
        docs/djblets/guides/avatars/index.rst
        docs/djblets/guides/registries/writing-registries.rst
    
    
    
    Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        docs/djblets/guides/index.rst
        djblets/avatars/services/__init__.py
        docs/djblets/guides/avatars/writing-avatar-services.rst
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
        docs/djblets/guides/avatars/index.rst
        docs/djblets/guides/registries/writing-registries.rst
    
    
  2. 
      
brennie
david
  1. Ship It!
  2. 
      
chipx86
  1. Huh, was sure I published this...

  2. djblets/avatars/registry.py (Diff revision 8)
     
     
     

    Description on the next line.

  3. djblets/avatars/registry.py (Diff revision 8)
     
     
     
     
     
     

    The else is implied, so you can remove it. The first part is a guard, and doesn't have to be connected to the rest.

    Same with ones below.

  4. djblets/avatars/registry.py (Diff revision 8)
     
     
     
     

    You can remove the blank line here.

  5. djblets/avatars/registry.py (Diff revision 8)
     
     

    This is already saved below.

    Also, are there cases where we don't need to save?

  6. djblets/avatars/services/base.py (Diff revision 8)
     
     

    Thinking this should be avatar_id. Some of our older stuff uses id (which is a reserved word), and most of our newer stuff uses things like avatar_id.

  7. djblets/avatars/services/base.py (Diff revision 8)
     
     

    This is going to be a SafeText.

  8. djblets/avatars/tests.py (Diff revision 8)
     
     
     
     

    Blank line not needed.

  9. djblets/registries/registry.py (Diff revision 8)
     
     
     

    Having a hard time parsing this. Maybe "The keyword arguments passed to the error-specific formatting string."

  10. djblets/registries/registry.py (Diff revision 8)
     
     

    No need for None.

  11. djblets/registries/registry.py (Diff revision 8)
     
     

    Let's include the class name in this (using __class__).

  12. djblets/static/djblets/js/jquery.gravy.retina.js (Diff revision 8)
     
     
     
     

    No blank line.

  13. djblets/util/templatetags/djblets_images.py (Diff revision 8)
     
     
     
     
     
     

    We shouldn't assume anything about the safeness of the URLs. If the URL has a & in it, then it'll turn into an entity here. So instead, let's do:

    return format_html_join(
        ', '
        '{0} {1}',
        (
            (url, descriptor)
            for descriptor, url in ...
            if url is ...
        ))
    
    1. There's actually another place like this where we assume safety of the gravatar URL, whereas the old templates I don't think did that? I don't think we shouldn't assume safety of those URLs, but we should test that.

    2. No they won't.

      In [3]: from django.utils.html import mark_safe
      
      In [4]: mark_safe('http://foo/y?a=b&c=d')
      
      Out[4]: 'http://foo/y?a=b&c=d'
      

      Also, documentation explicitly states that all URLs returned from avatar services must be marked safe text, so this should be fine.

      Get back to me re: this and we can hash it out on Monday.

    3. I really have no earthly idea what I was thinking when I wrote that. I don't think I read the code I thought I read.

  14. 
      
brennie
reviewbot
  1. Tool: PEP8 Style Checker
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        docs/djblets/guides/index.rst
        djblets/avatars/services/__init__.py
        docs/djblets/guides/avatars/writing-avatar-services.rst
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
        docs/djblets/guides/avatars/index.rst
        docs/djblets/guides/registries/writing-registries.rst
    
    
    
    Tool: Pyflakes
    Processed Files:
        djblets/registries/registry.py
        djblets/util/templatetags/djblets_images.py
        djblets/avatars/services/gravatar.py
        djblets/avatars/services/base.py
        djblets/avatars/errors.py
        djblets/avatars/tests.py
        djblets/avatars/registry.py
    
    Ignored Files:
        djblets/avatars/templates/avatars/avatar.html
        djblets/static/djblets/js/jquery.gravy.retina.js
        docs/djblets/guides/index.rst
        djblets/avatars/services/__init__.py
        docs/djblets/guides/avatars/writing-avatar-services.rst
        djblets/avatars/__init__.py
        docs/djblets/coderef/index.rst
        docs/djblets/guides/avatars/index.rst
        docs/djblets/guides/registries/writing-registries.rst
    
    
  2. 
      
brennie
Review request changed

Status: Closed (submitted)

Change Summary:

Pushed to release-0.10.x (d940248)
Loading...