Skip to content

Commit

Permalink
Merge pull request #45 from pluralsight/show_contributors
Browse files Browse the repository at this point in the history
Show contributors
  • Loading branch information
durden committed Apr 14, 2016
2 parents 6810bdc + 1544d13 commit e056d2b
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 1 deletion.
80 changes: 79 additions & 1 deletion pskb_website/models/article.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,13 @@ def read_article(path, rendered_text=True, branch=u'master', repo_path=None,
# We don't have a ton of cache space so reserve it for more
# high-traffic data like the rendered view of the articles.
if rendered_text:
# Force read of contributors and only cache it for published
# guides. Again, trying to save on cache space, and we're not too
# concerned with the list of contributors until a guide is
# published.
if article.published:
article._read_contributors_from_api()

cache.save_file(article.path, article.branch, lib.to_json(article))
else:
# We cannot properly show an article without metadata.
Expand Down Expand Up @@ -587,7 +594,7 @@ def save_article_meta_data(article, author_name, email, branch=None):
# Don't need to serialize everything, just the important stuff that's not
# stored in the path and article.
exclude_attrs = ('content', 'external_url', 'sha', 'repo_path', '_path',
'last_updated')
'last_updated', '_contributors')
json_content = lib.to_json(article, exclude_attrs=exclude_attrs)

# Nothing changed so no commit needed
Expand Down Expand Up @@ -888,6 +895,10 @@ def __init__(self, title, author_name, filename=ARTICLE_FILENAME,
self._path = None
self._publish_status = DRAFT

# List of User objects representing any 'author' i.e user who has
# contributed at least 1 line of text to this article.
self._contributors = []

@property
def path(self):
return u'%s/%s/%s' % (self.publish_status,
Expand Down Expand Up @@ -918,6 +929,29 @@ def publish_status(self, new_status):
def published(self):
return self.publish_status == PUBLISHED

@property
def contributors(self):
"""
List of tuples representing any 'author' i.e user who has contributed
at least 1 line of text to this article. Each tuple is in the form of
(name, login) where name can be None.
We use plain tuples instead of named tuples or User objects so we can
easily seralize the contributors to JSON.
"""

# Small form of caching. This way we only fetch the contributors once.

# NOTE: This could result in some data out of data if we have new
# contributors after this is called but contributor information isn't
# super important so should be ok.
if self._contributors:
return self._contributors

self._read_contributors_from_api()

return self._contributors

@staticmethod
def from_json(str_):
"""
Expand Down Expand Up @@ -967,3 +1001,47 @@ def full_path(self):
:returns: Full path to article
"""
return '%s/%s/%s' % (self.repo_path, self.path, self.filename)

def _read_contributors_from_api(self):
"""Force reset of contributors for article and fetch from github API"""

self._contributors = []

# Keep track of all the logins that have names so we can only store
# users with their full names if available. Some contributions maybe
# returned from the API with a full name and without a full name, just
# depends on how the commit was done.
logins_with_names = set()

# We have to request the contributors for published and in-review
# statuses if the article is published. This is a quirk to how the
# github commit API works. The API doesn't use git --follow so since
# guides are moved from in-review to published we have to find any
# authors at both locations.
# We don't bother with requesting 'draft' status b/c we're assuming
# only authors work on the guide in that phase.

# Use set to track uniques but we'll turn it into a list at the end so
# we can make sure we use a serializable type.
unique_contributors = set()

for status in (PUBLISHED, IN_REVIEW):
path = u'%s/%s/%s' % (status,
utils.slugify_stack(self.stacks[0]),
utils.slugify(self.title))

contribs = remote.file_contributors(path, branch=self.branch)

# remote call returns committers as well but we're only interested
# in authors
for user in contribs['authors']:
if user[1] != self.author_name:
if user[0] is not None:
logins_with_names.add(user[1])

unique_contributors.add(user)

# Remove any duplicates that have empty names
for user in unique_contributors:
if user[0] is not None or user[1] not in logins_with_names:
self._contributors.append(user)
53 changes: 53 additions & 0 deletions pskb_website/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,59 @@ def merge_branch(repo_path, base, head, message):
return False


def file_contributors(path, branch=u'master'):
"""
Get dictionary of User objects representing authors and committers to a
file
:param path: Short-path to file (<dir>/.../<filename>) i.e. without repo
and owner
:param base: Name of branch to read contributors for
:returns: Dictionary of the following form::
{'authors': set([(name, login), (name, login), ...]),
'committers': set([(name, login), (name, login), ...])}
Note that name can be None if user doesn't have their full name setup on
github account.
"""

contribs = {'authors': set(), 'committers': set()}
url = u'/repos/%s/commits' % (default_repo_path())

app.logger.debug('GET: %s path: %s, branch: %s', url, path, branch)

resp = github.get(url, data={'path': path, 'branch': branch})
if resp.status != 200:
log_error('Failed reading commits from github', url, resp)
return contribs

def _extract_data_from_commit(commit, key):
login = commit[key]['login']

try:
author_name = commit['commit'][key]['name']
except KeyError:
author_name = None
else:
if not author_name:
author_name = None

# API can return same name and login depending on how the account and
# commit information is setup so don't bother storing duplicates. This
# way caller knows we didn't get a real author name.
if login == author_name:
author_name = None

return (author_name, commit[key]['login'])

for commit in resp.data:
contribs['authors'].add(_extract_data_from_commit(commit, 'author'))
contribs['committers'].add(_extract_data_from_commit(commit, 'committer'))

return contribs


def contributor_stats(repo_path=None):
"""
Get response of /repos/<repo_path>/stats/contributors from github.com
Expand Down
7 changes: 7 additions & 0 deletions pskb_website/static/js/editor_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,13 @@ function save(sha, path, secondary_repo) {
data: data,
dataType: 'json',
cache: false,
beforeSend: function(xhr) {
$('html, body').css("cursor", "wait");
return true;
},
complete: function(xhr, txt_status) {
$('html, body').css("cursor", "auto");
},
success: function(data) {
closeFullscreen();
console.log(data);
Expand Down
22 changes: 22 additions & 0 deletions pskb_website/templates/article.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ <h1 id="title" class="lead">{{article.title}}</h1>

{{article.content|safe}}

{# This is an extra API request so only do it for published guides. #}
{% if article.published and article.contributors %}
<div id="contributors" class="row">
<div class="col-sm-12">
<h4>Contributors</h4>
<p>
Thanks to the following users who've contributed to
making this the best guide possible!
</p>
<ul>
{% for name, login in article.contributors %}
<li><a href="https://github.com/{{login}}" target="_blank">{{name if name else login}}</a></li>
{% endfor %}
</ul>
</div>
<p>
Have an idea for improving this guide? <a href="{{url_for('write', article_path=article.path, branch=article.branch)}}">Edit this guide</a>
to get on the list!
</p>
</div>
{% endif %}

<div id="user-info" class="row">
<div class="col-sm-4">
{% if user.avatar_url %}
Expand Down

0 comments on commit e056d2b

Please sign in to comment.