Repository: pypa/pip

Issue:
Add email link on pip's documentation
**What?**
We want to add a way for users to give feedback on the pip docs, _while_ they are using them.

**Why?**
Our goal is to understand what problem the user is trying to solve, what information is helpful, what is unhelpful, and what is missing.  Understanding this will help us improve the pip docs for future users.

**How?**
Initially, we want to add an email link to each documentation page to encourage users to provide us with feedback.  This should be a standout box, and contain a message that is something like this:

> ### Did this article help?
> We are currently doing research to improve pip's documentation and would love your feedback.
> Please [email us](emailaddresstbc) and let us know:
> 1. What problem were you trying to solve when you came to this page?
> 2. What content was useful?
> 3. What content was not useful?

**Where?**
It would be great to include this message either:
- At the end of each article
- As a "sticky" message either at the top or right hand side of the page



Discussion:
ping @jdittrich for feedback on the message.
Note: it would be great for the email link to include the questions we want answered [in the email body](https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Creating_hyperlinks#Specifying_details).
@webknjaz or @yeraydiazdiaz - would either of you have time to work on this?  I'd like to start collecting data asap.

@nlhkabu would something like https://sphinx-version-warning.rtfd.io work?
@webknjaz 
Is this the banner that would hold the email link?
![image](https://user-images.githubusercontent.com/3416487/91737677-ab105580-ebaf-11ea-891b-ed88486f90fa.png)

@webknjaz @nlhkabu 
I think it would be preferable to have it in the sidebar, as people might want to give feedback on something they deal with when having scrolled down. Alternatively: Could the banner appear at the bottom of the page? (I guess there it is more likely that the participants already made some experiences with the text and could give feedback on it, whereas, if the banner is on top, they need to remember to give feedback later while solving their problem (or whatever they consult the docs for))
> Is this the banner that would hold the email link?

Yes, but if not — making a sticky header shouldn't be too hard.

> I think it would be preferable to have it in the sidebar

As in, under the left menu?

FWIW this issue looks like a temporary solution so I'd avoid investing too much time into it knowing that it'll be replaced.
@webknjaz @jdittrich how about a compromise.  We include the header at the top and the bottom of the page?
As @webknjaz mentioned, it's a temporary fix, so I don't think we should invest too much time in it.

@jdittrich are you happy with this text?

> ### Did this article help?
> We are currently doing research to improve pip's documentation and would love your feedback.
> Please [email us](emailaddresstbc) and let us know:
> 1. What problem were you trying to solve when you came to this page?
> 2. What content was useful?
> 3. What content was not useful?

I will confirm email address ASAP.
>  We include the header at the top and the bottom of the page?

Yes, good compromise :)
Email address has been created: docs-feedback@pypa.io !
I've started something at #8848 but it's totally unstyled atm. Decided not to use an external ext.

@nlhkabu do you want full page URLs injected into the widget? The extension I started does not have this info during the build time, it only has the doc name. Adding full URLs will require some client-side JS on top.

Resolving patch:
diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py
new file mode 100644
--- /dev/null
+++ b/docs/docs_feedback_sphinxext.py
@@ -0,0 +1,165 @@
+"""A sphinx extension for collecting per doc feedback."""
+
+from __future__ import annotations
+
+from itertools import chain
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Dict, List, Union
+
+    from sphinx.application import Sphinx
+
+
+DEFAULT_DOC_LINES_THRESHOLD = 250
+RST_INDENT = 4
+EMAIL_INDENT = 6
+
+
+def _modify_rst_document_source_on_read(
+        app: Sphinx,
+        docname: str,
+        source: List[str],
+) -> None:
+    """Add info block to top and bottom of each document source.
+
+    This function modifies RST source in-place by adding an admonition
+    block at the top and the bottom of each document right after it's
+    been read from disk preserving :orphan: at top, if present.
+    """
+    admonition_type = app.config.docs_feedback_admonition_type
+    big_doc_lines = app.config.docs_feedback_big_doc_lines
+    escaped_email = app.config.docs_feedback_email.replace(' ', r'\ ')
+    excluded_documents = set(app.config.docs_feedback_excluded_documents)
+    questions_list = app.config.docs_feedback_questions_list
+
+    valid_admonitions = {
+        'attention', 'caution', 'danger', 'error', 'hint',
+        'important', 'note', 'tip', 'warning', 'admonition',
+    }
+
+    if admonition_type not in valid_admonitions:
+        raise ValueError(
+            'Expected `docs_feedback_admonition_type` to be one of '
+            f'{valid_admonitions} but got {admonition_type}.'
+        )
+
+    if not questions_list:
+        raise ValueError(
+            'Expected `docs_feedback_questions_list` to list questions '
+            'but got none.'
+        )
+
+    if docname in excluded_documents:
+        # NOTE: Completely ignore any document
+        # NOTE: listed in 'docs_feedback_excluded_documents'.
+        return
+
+    is_doc_big = source[0].count('\n') >= big_doc_lines
+
+    questions_list_rst = '\n'.join(
+        f'{" " * RST_INDENT}{number!s}. {question}'
+        for number, question in enumerate(questions_list, 1)
+    )
+    questions_list_urlencoded = (
+        '\n'.join(
+            f'\n{" " * RST_INDENT}{number!s}. {question} '
+            for number, question in enumerate(
+                chain(
+                    (f'Document: {docname}. Page URL: https://', ),
+                    questions_list,
+                ),
+            )
+        ).
+        rstrip('\r\n\t ').
+        replace('\r', '%0D').
+        replace('\n', '%0A').
+        replace(' ', '%20')
+    )
+
+    admonition_msg = rf"""
+    **Did this article help?**
+
+    We are currently doing research to improve pip's documentation
+    and would love your feedback.
+    Please `email us`_ and let us know{{let_us_know_ending}}
+
+{{questions_list_rst}}
+
+    .. _email us:
+       mailto:{escaped_email}\
+       ?subject=[Doc:\ {docname}]\ Pip\ docs\ feedback\ \
+       (URL\:\ https\://)\
+       &body={questions_list_urlencoded}
+    """
+    let_us_know_ending = ':'
+
+    info_block_bottom = (
+        f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n'
+    )
+
+    questions_list_rst = ''
+    let_us_know_ending = (
+        ' why you came to this page and what on it helped '
+        'you and what did not.    '
+        '(:issue:`Read more about this research <8517>`)'
+    )
+    info_block_top = '' if is_doc_big else (
+        f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n'
+    )
+
+    orphan_mark = ':orphan:'
+    is_orphan = orphan_mark in source[0]
+    if is_orphan:
+        source[0].replace(orphan_mark, '')
+    else:
+        orphan_mark = ''
+
+    source[0] = '\n\n'.join((
+        orphan_mark, info_block_top, source[0], info_block_bottom,
+    ))
+
+
+def setup(app: Sphinx) -> Dict[str, Union[bool, str]]:
+    """Initialize the Sphinx extension.
+
+    This function adds a callback for modifying the document sources
+    in-place on read.
+
+    It also declares the extension settings changable via :file:`conf.py`.
+    """
+    rebuild_trigger = 'html'  # rebuild full html on settings change
+    app.add_config_value(
+        'docs_feedback_admonition_type',
+        default='important',
+        rebuild=rebuild_trigger,
+    )
+    app.add_config_value(
+        'docs_feedback_big_doc_lines',
+        default=DEFAULT_DOC_LINES_THRESHOLD,
+        rebuild=rebuild_trigger,
+    )
+    app.add_config_value(
+        'docs_feedback_email',
+        default='Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>',
+        rebuild=rebuild_trigger,
+    )
+    app.add_config_value(
+        'docs_feedback_excluded_documents',
+        default=set(),
+        rebuild=rebuild_trigger,
+    )
+    app.add_config_value(
+        'docs_feedback_questions_list',
+        default=(),
+        rebuild=rebuild_trigger,
+    )
+
+    app.add_css_file('important-admonition.css')
+    app.connect('source-read', _modify_rst_document_source_on_read)
+
+    return {
+        'parallel_read_safe': True,
+        'parallel_write_safe': True,
+        'version': 'builtin',
+    }
diff --git a/docs/html/conf.py b/docs/html/conf.py
--- a/docs/html/conf.py
+++ b/docs/html/conf.py
@@ -31,10 +31,14 @@
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 # extensions = ['sphinx.ext.autodoc']
 extensions = [
+    # native:
     'sphinx.ext.extlinks',
-    'pip_sphinxext',
     'sphinx.ext.intersphinx',
+    # third-party:
     'sphinx_tabs.tabs',
+    # in-tree:
+    'docs_feedback_sphinxext',
+    'pip_sphinxext',
 ]
 
 # intersphinx
@@ -177,7 +181,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = []
+html_static_path = ['_static']
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
@@ -308,3 +312,19 @@ def to_document_name(path, base_dir):
     )
 
     man_pages.append((fname_base, outname, description, u'pip developers', 1))
+
+# -- Options for docs_feedback_sphinxext --------------------------------------
+
+# NOTE: Must be one of 'attention', 'caution', 'danger', 'error', 'hint',
+# NOTE: 'important', 'note', 'tip', 'warning' or 'admonition'.
+docs_feedback_admonition_type = 'important'
+docs_feedback_big_doc_lines = 50  # bigger docs will have a banner on top
+docs_feedback_email = 'Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>'
+docs_feedback_excluded_documents = {  # these won't have any banners
+    'news',
+}
+docs_feedback_questions_list = (
+    'What problem were you trying to solve when you came to this page?',
+    'What content was useful?',
+    'What content was not useful?',
+)


Test patch: