diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml old mode 100644 new mode 100755 index 0921871155..8df3f18fd9 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,6 +69,8 @@ jobs: texlive-fonts-recommended - name: Generate build files + env: + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} run: | make prepare diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 6bb56e3ea5..76112c7704 --- a/README.md +++ b/README.md @@ -25,6 +25,14 @@ Open the HTML manual in a web brower for visual verification firefox build/manual/build/html/index.html +### HTML pages with contributor information + +To build the html pages that contains contributor information on the sidebar set a github access token to an environment variable called GH_TOKEN. + +Note: If set this will increase the build time of the documentation by approximately a factor of 4. + + setenv GH_TOKEN + ## Repository content These are the components that define the structure of NeXus data files diff --git a/dev_tools/docs/nxdl.py b/dev_tools/docs/nxdl.py old mode 100644 new mode 100755 index 8da3ebbc05..e5163c51e0 --- a/dev_tools/docs/nxdl.py +++ b/dev_tools/docs/nxdl.py @@ -12,6 +12,7 @@ from ..globals.errors import NXDLParseError from ..globals.nxdl import NXDL_NAMESPACE from ..globals.urls import REPO_URL +from ..utils.github import get_file_contributors_via_api from ..utils.types import PathLike from .anchor_list import AnchorRegistry @@ -80,6 +81,7 @@ def _parse_nxdl_file(self, nxdl_file: Path): self._print( f".. auto-generated by {__name__} from the NXDL source {source} -- DO NOT EDIT" ) + self._print("") self._print(".. index::") self._print(f" ! {nxclass_name} ({self._listing_category})") @@ -100,6 +102,24 @@ def _parse_nxdl_file(self, nxdl_file: Path): else: extends = f":ref:`{extends}`" + # add the contributors as variables to the rst file that will + nxdl_root = get_nxdl_root() + rel_path = str(nxdl_file.relative_to(nxdl_root)) + rel_html = str(rel_path).replace(os.sep, "/") + contribs_dct = get_file_contributors_via_api("definitions", rel_html) + if contribs_dct is not None: + self._print("") + self._print("..") + self._print(" Contributors List") + for date_str, contrib_dct in contribs_dct.items(): + date_str = date_str.split("T")[0] + name = contrib_dct["name"] + gh_login_nm = contrib_dct["commit_dct"]["committer"]["login"] + gh_avatar_url = contrib_dct["commit_dct"]["committer"]["avatar_url"] + self._print("") + s = "|".join([name, gh_login_nm, gh_avatar_url, date_str]) + self._print(f" .. |contrib_name| replace:: {s}") + self._print("") self._print("**Status**:\n") self._print(f" {self._listing_category.strip()}, extends {extends}") diff --git a/dev_tools/ext/__init__.py b/dev_tools/ext/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/dev_tools/ext/contrib_ext.py b/dev_tools/ext/contrib_ext.py new file mode 100755 index 0000000000..dc37de88c2 --- /dev/null +++ b/dev_tools/ext/contrib_ext.py @@ -0,0 +1,30 @@ +import re + +# a custom sphinx extension that is connected to the source-read hook for rst files, +# the purpose is to read all of the contributor information from the rst file and +# place it in a string variable that will be used in the sourcelink.html jinja template +# that has been over ridden and sits in the _templates directory to produce the +# contributor information on the for right sidebar of the html pages + +variables_re = re.compile(r"\|(\w+)\| replace::\s(.+)") + + +def extract_contributor_vars(app, docname, source): + # Read the RST file content + content = source[0] + + # Extract variables using regular expressions + variables = variables_re.findall(content) + + # Create a dictionary to store the extracted variables + # this will create a list of single strings each of which contain the info about the contributor + extracted_variables = [var[1] for var in variables] + if "variables" not in app.config.html_context.keys(): + app.config.html_context["variables"] = {} + + # Add the extracted variables to the Jinja environment + app.config.html_context["variables"][docname] = extracted_variables + + +def setup(app): + app.connect("source-read", extract_contributor_vars) diff --git a/dev_tools/utils/github.py b/dev_tools/utils/github.py new file mode 100755 index 0000000000..f6cb1d2374 --- /dev/null +++ b/dev_tools/utils/github.py @@ -0,0 +1,92 @@ +import os + +import requests + + +def format_author_name(nm): + """ + make sure all words in name start with a capital + """ + nms = nm.split(" ") + auth_nm = " ".join([n.capitalize() for n in nms]) + return auth_nm + + +def get_github_profile_name(email): + """ + given an email addr return the github login name + """ + email = email.replace(" ", "") + nm = email.split("@")[0] + return nm + + +def get_file_contributors_via_api(repo_name, file_path): + """ + This function takes the repo name (ex:"definitions") and relative path to the nxdl + file (ex: "applications/NXmx.nxdl.xml") and using the github api it retrieves a dictionary + of committers for that file in descending date order. + + In order to increase the capacity (rate) of use of the github API an access token is used if it exists + as an environment variable called GH_TOKEN, in the ci yaml file this is expected to be assigned from the secret + object like this + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + With the access token the rate is 5000 times per hour and without it is 60 + + returns a sorted dict of unique contributors to a file, or None if no GH_TOKEN has been defined in os.environ + """ + have_token = False + if "GH_TOKEN" in os.environ.keys(): + access_token = os.environ["GH_TOKEN"] + if len(access_token) > 0: + have_token = True + else: + # because the environment does not contain GH_TOKEN, assume the user wants to build the + # docs without contributor info + return None + + contrib_skip_list = ["GitHub"] + url = f"https://api.github.com/repos/nexusformat/{repo_name}/commits" + params = {"path": file_path} + headers = {} + if have_token: + # Set the headers with the access token + headers = { + "Authorization": f"token {access_token}", + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(url, params=params, headers=headers) + commits = response.json() + if response.status_code != 200: + # if its 403: the max rate per hour has been reached + raise Exception( + f"access_token={access_token}, {commits['message']},{commits['documentation_url']}" + ) + + contributor_names = set() + contribs_dct = {} + _email_lst = [] + for commit_dct in commits: + if commit_dct["committer"] is not None: + contributor = commit_dct["commit"]["committer"]["name"] + if contributor in contrib_skip_list: + continue + contributor_names.add(contributor) + if commit_dct["commit"]["committer"]["email"] not in _email_lst: + _email = commit_dct["commit"]["committer"]["email"] + _email_lst.append(_email) + contribs_dct[commit_dct["commit"]["committer"]["date"]] = { + "name": format_author_name( + commit_dct["commit"]["committer"]["name"] + ), + "commit_dct": commit_dct, + } + + # sort them so they are in descending order from newest to oldest + sorted_keys = sorted(contribs_dct.keys(), reverse=True) + sorted_dict = {key: contribs_dct[key] for key in sorted_keys} + + return sorted_dict diff --git a/manual/source/_templates/sourcelink.html b/manual/source/_templates/sourcelink.html new file mode 100755 index 0000000000..791bdb3348 --- /dev/null +++ b/manual/source/_templates/sourcelink.html @@ -0,0 +1,45 @@ +{# + basic/sourcelink.html + ~~~~~~~~~~~~~~~~~~~~~ + + Sphinx sidebar template: "show source" link. + + :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +#} +{%- if show_source and has_source and sourcename %} +
+

{{ _('This Page') }}

+

Have a Question? Get help

+ +
+{%- endif %} diff --git a/manual/source/conf.py b/manual/source/conf.py old mode 100644 new mode 100755 index 6b584d6da5..1471297783 --- a/manual/source/conf.py +++ b/manual/source/conf.py @@ -8,14 +8,13 @@ import sys, os, datetime + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - +# add the abs path to the custom extension for collecting the contributor variables from the rst files +sys.path.insert(0, os.path.abspath('../../../dev_tools/ext')) # -- Project information ----------------------------------------------------- @@ -47,7 +46,8 @@ 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', 'sphinx.ext.todo', - 'sphinx_tabs.tabs' + 'sphinx_tabs.tabs', + 'contrib_ext' ] # Show `.. todo` directives in the output @@ -80,10 +80,10 @@ html_sidebars = { '**': [ - 'localtoc.html', - 'relations.html', - 'sourcelink.html', - 'searchbox.html', + 'localtoc.html', + 'relations.html', + 'sourcelink.html', + 'searchbox.html', 'google_search.html' ], }