mirror of https://github.com/zulip/zulip.git
Add /authors page.
Contributor visualization showing the avatar, user name and number of commits for each contributors. The JSON data would be updated upon deployment, triggered by the `update-prod-static` script.
This commit is contained in:
parent
747f66bfe1
commit
c407919db3
|
@ -18,6 +18,7 @@ coverage/
|
|||
/zproject/dev-secrets.conf
|
||||
static/js/bundle.js
|
||||
static/generated/emoji
|
||||
static/generated/github-contributors.json
|
||||
static/locale/language_options.json
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
|
|
|
@ -503,6 +503,48 @@ a.bottom-signup-button {
|
|||
font-size: 25px;
|
||||
}
|
||||
|
||||
.authors_row {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.authors_row a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.authors_row td {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 120px;
|
||||
padding: 10px;
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.authors_row td:hover {
|
||||
color: #00796B;
|
||||
box-shadow: 0px 0px 30px #B2DFDB;
|
||||
}
|
||||
|
||||
.authors_row .avatar {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.authors_row .info {
|
||||
width: 50%;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.avatar_img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
.integration-lozenges {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -568,12 +610,13 @@ a.bottom-signup-button {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.login-page-header,
|
||||
.api-page-header,
|
||||
.apps-page-header,
|
||||
.authors-page-header,
|
||||
.feature-page-header,
|
||||
.integrations-page-header,
|
||||
.register-page-header,
|
||||
.api-page-header {
|
||||
.login-page-header,
|
||||
.register-page-header {
|
||||
font-weight: 300;
|
||||
font-size: 40px;
|
||||
display: block;
|
||||
|
@ -594,11 +637,11 @@ a.bottom-signup-button {
|
|||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.feature-page-header,
|
||||
.api-page-header,
|
||||
.apps-page-header,
|
||||
.integrations-page-header
|
||||
{
|
||||
.authors-page-header,
|
||||
.feature-page-header,
|
||||
.integrations-page-header {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "zerver/portico.html" %}
|
||||
|
||||
{% block portico_content %}
|
||||
<div class="authors-page-header">
|
||||
<i class='icon-vector-github'></i> Contributors
|
||||
</div>
|
||||
<div class="container">
|
||||
{% for row in data %}
|
||||
<div class="row">
|
||||
{% for group in (row[0:2], row[2:4]) %}
|
||||
<div class="span6">
|
||||
{#
|
||||
Using tables here is a hack to work around the inflexible bootstrap v2.1.1
|
||||
grid system.
|
||||
#}
|
||||
<table class="authors_row">
|
||||
<tr>
|
||||
{% for person in group %}
|
||||
<td>
|
||||
{% if person %}
|
||||
<a href="https://github.com/{{ person.name }}">
|
||||
<div class="avatar float-left">
|
||||
<img class="avatar_img" src="{{ person.avatar }}" alt="Avatar" />
|
||||
</div>
|
||||
<div class='info float-right'>
|
||||
<b>@{{ person.name }}</b><br />
|
||||
{{ person.commits }} commits
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="span12">
|
||||
Statistic last Updated: {{ date }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -219,6 +219,7 @@ def main(options):
|
|||
run(["sudo", "chown", "%s:%s" % (user_id, user_id), EMOJI_CACHE_PATH])
|
||||
run(["tools/setup/emoji/build_emoji"])
|
||||
run(["scripts/setup/generate_secrets.py", "--development"])
|
||||
run(["tools/update-authors-json", "--use-fixture"])
|
||||
if options.is_travis and not options.is_production_travis:
|
||||
run(["sudo", "service", "rabbitmq-server", "restart"])
|
||||
run(["sudo", "service", "redis-server", "restart"])
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
Fetch contributors data from Github using their API, convert it to structured
|
||||
JSON data for the /authors page.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
from typing import Any, Dict, List, Optional, Union, Text
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import date
|
||||
import subprocess
|
||||
|
||||
from six.moves import range
|
||||
import requests
|
||||
import simplejson as json
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'zproject.settings'
|
||||
from django.conf import settings
|
||||
from zerver.lib.utils import split_by
|
||||
|
||||
FIXTURE_FILE = os.path.join(os.path.dirname(__file__), '../zerver/fixtures/authors.json')
|
||||
GITHUB_LINK = 'https://api.github.com/repos/zulip/zulip/stats/contributors'
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--max-retries', type=int, default=3,
|
||||
help='Number of times to retry fetching data from Github')
|
||||
# In Travis CI and development environment, we use test fixture to avoid
|
||||
# fetching from Github constantly.
|
||||
parser.add_argument('--use-fixture', action='store_true', default=False,
|
||||
help='Use fixture data instead of fetching from Github')
|
||||
args = parser.parse_args()
|
||||
|
||||
def fetch_data(retries, link):
|
||||
# type: (int, str) -> Optional[List[Dict[str, Any]]]
|
||||
for _ in range(retries):
|
||||
try:
|
||||
r = requests.get(link) # type: requests.Response
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
else:
|
||||
print('Github API return non 200 status.')
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(e)
|
||||
|
||||
return None
|
||||
|
||||
def write_to_disk(json_data, out_file):
|
||||
# type: (Dict[str, Any], str) -> None
|
||||
with open(out_file, 'w') as f:
|
||||
try:
|
||||
f.write("{}\n".format(json.dumps(json_data)))
|
||||
except IOError as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
def run_production():
|
||||
# type: () -> None
|
||||
"""
|
||||
Fetch data from Github and stored it in
|
||||
`static/generated/github-contributors.json`
|
||||
"""
|
||||
json_data = fetch_data(args.max_retries, GITHUB_LINK) # type: Optional[List[Dict[str, Any]]]
|
||||
if json_data:
|
||||
# Successfully fetch data from Github
|
||||
contribs = []
|
||||
for user in json_data:
|
||||
author = user.get('author')
|
||||
result_user = dict(
|
||||
avatar=author.get('avatar_url'),
|
||||
name=author.get('login'),
|
||||
commits=user.get('total')
|
||||
)
|
||||
contribs.append(result_user)
|
||||
|
||||
out_contrib_data = split_by(
|
||||
sorted(contribs, key=lambda k: k.get('commits'), reverse=True),
|
||||
4, None
|
||||
) # type: List[List[Optional[Dict[str, Union[Text, int]]]]]
|
||||
|
||||
out_data = dict(
|
||||
data=out_contrib_data,
|
||||
date=str(date.today())
|
||||
) # type: Dict[str, Any]
|
||||
|
||||
write_to_disk(out_data, settings.CONTRIBUTORS_DATA)
|
||||
|
||||
else:
|
||||
print('Fail to fetch data from Github.')
|
||||
sys.exit(1)
|
||||
|
||||
def copy_fixture():
|
||||
# type: () -> None
|
||||
"""
|
||||
Copy test fixture file from zerver/fixtures. This is used to avoid
|
||||
constantly fetching data from Github during testing.
|
||||
"""
|
||||
subprocess.check_call(['cp', FIXTURE_FILE, settings.CONTRIBUTORS_DATA])
|
||||
|
||||
if args.use_fixture:
|
||||
copy_fixture()
|
||||
else:
|
||||
run_production()
|
|
@ -64,4 +64,7 @@ subprocess.check_call(['cp', '-a', 'static/locale',
|
|||
os.path.join(settings.STATIC_ROOT, 'locale')],
|
||||
stdout=fp, stderr=fp)
|
||||
|
||||
# Generate /authors page markdown
|
||||
subprocess.check_call(['./tools/update-authors-json'], stdout=fp)
|
||||
|
||||
fp.close()
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"date": "2050-11-5",
|
||||
"data": [
|
||||
[{
|
||||
"commits": 4131,
|
||||
"name": "timabbott",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/2746074?v=3"
|
||||
}, {
|
||||
"commits": 1102,
|
||||
"name": "zbenjamin",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/58684?v=3"
|
||||
}, {
|
||||
"commits": 704,
|
||||
"name": "wdaher",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/109246?v=3"
|
||||
}, {
|
||||
"commits": 671,
|
||||
"name": "lfranchi",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/67976?v=3"
|
||||
}],
|
||||
[{
|
||||
"commits": 524,
|
||||
"name": "showell",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/142908?v=3"
|
||||
}, {
|
||||
"commits": 425,
|
||||
"name": "lfaraone",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/73410?v=3"
|
||||
}, {
|
||||
"commits": 385,
|
||||
"name": "sharmaeklavya2",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/3839472?v=3"
|
||||
}, {
|
||||
"commits": 295,
|
||||
"name": "umairwaheed",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/641763?v=3"
|
||||
}]
|
||||
]
|
||||
}
|
|
@ -10,11 +10,12 @@ import hashlib
|
|||
import heapq
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
from time import sleep
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
from six.moves import range
|
||||
from six.moves import range, map, zip_longest
|
||||
from zerver.lib.str_utils import force_text
|
||||
|
||||
T = TypeVar('T')
|
||||
|
@ -204,3 +205,12 @@ def check_subdomain(realm_subdomain, user_subdomain):
|
|||
if realm_subdomain != user_subdomain:
|
||||
return False
|
||||
return True
|
||||
|
||||
def split_by(array, group_size, filler):
|
||||
# type: (List[Any], int, Any) -> List[List[Any]]
|
||||
"""
|
||||
Group elements into list of size `group_size` and fill empty cells with
|
||||
`filler`. Recipe from https://docs.python.org/3/library/itertools.html
|
||||
"""
|
||||
args = [iter(array)] * group_size
|
||||
return list(map(list, zip_longest(*args, fillvalue=filler)))
|
||||
|
|
|
@ -37,6 +37,7 @@ from zerver.lib.notifications import handle_missedmessage_emails
|
|||
from zerver.lib.session_user import get_session_dict_user
|
||||
from zerver.middleware import is_slow_query
|
||||
from zerver.lib.avatar import avatar_url
|
||||
from zerver.lib.utils import split_by
|
||||
|
||||
from zerver.worker import queue_processors
|
||||
|
||||
|
@ -50,6 +51,7 @@ import time
|
|||
import ujson
|
||||
import random
|
||||
import filecmp
|
||||
import subprocess
|
||||
|
||||
def bail(msg):
|
||||
# type: (str) -> None
|
||||
|
@ -2129,6 +2131,28 @@ class HomeTest(ZulipTestCase):
|
|||
result = self.client_get("/api/v1/generate_204")
|
||||
self.assertEqual(result.status_code, 204)
|
||||
|
||||
class AuthorsPageTest(ZulipTestCase):
|
||||
def setUp(self):
|
||||
# type: () -> None
|
||||
""" Manual installation which did not execute `tools/provision.py`
|
||||
would not have the `static/generated/github-contributors.json` fixture
|
||||
file.
|
||||
"""
|
||||
if not os.path.exists(settings.CONTRIBUTORS_DATA):
|
||||
# Copy the fixture file in `zerver/fixtures` to `static/generated`
|
||||
update_script = os.path.join(os.path.dirname(__file__),
|
||||
'../../tools/update-authors-json')
|
||||
subprocess.check_call([update_script, '--use-fixture'])
|
||||
|
||||
def test_endpoint(self):
|
||||
# type: () -> None
|
||||
result = self.client_get('/authors/')
|
||||
self.assert_in_success_response(
|
||||
['Contributors', 'Statistic last Updated:', 'commits',
|
||||
'@timabbott'],
|
||||
result
|
||||
)
|
||||
|
||||
class MutedTopicsTests(ZulipTestCase):
|
||||
def test_json_set(self):
|
||||
# type: () -> None
|
||||
|
@ -2337,3 +2361,10 @@ class TestFindMyTeam(ZulipTestCase):
|
|||
result = self.client_post('/find_my_team/', data)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertIn("Please enter at most 10", result.content.decode('utf8'))
|
||||
|
||||
class UtilsUnitTest(TestCase):
|
||||
def test_split_by(self):
|
||||
# type: () -> None
|
||||
flat_list = [1, 2, 3, 4, 5, 6, 7]
|
||||
expected_result = [[1, 2], [3, 4], [5, 6], [7, None]]
|
||||
self.assertEqual(split_by(flat_list, 2, None), expected_result)
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
from __future__ import absolute_import
|
||||
from typing import Text, Union, Optional, Dict, Any, List, Tuple
|
||||
|
||||
import os
|
||||
import simplejson as json
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.shortcuts import redirect
|
||||
from django.conf import settings
|
||||
from six.moves import map
|
||||
|
||||
from zerver.decorator import has_request_variables, REQ, JsonableError, \
|
||||
|
@ -20,9 +25,8 @@ from zerver.lib.validator import check_bool, check_string
|
|||
from zerver.lib.utils import generate_random_token
|
||||
from zerver.models import UserProfile, Stream, Realm, Message, get_user_profile_by_email, \
|
||||
get_stream, email_allowed_for_realm
|
||||
from zproject.jinja2 import render_to_response
|
||||
|
||||
from typing import Text
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
def deactivate_user_backend(request, user_profile, email):
|
||||
# type: (HttpRequest, UserProfile, Text) -> HttpResponse
|
||||
|
@ -361,3 +365,15 @@ def get_profile_backend(request, user_profile):
|
|||
result['max_message_id'] = messages[0].id
|
||||
|
||||
return json_success(result)
|
||||
|
||||
def authors_view(request):
|
||||
# type: (HttpRequest) -> HttpResponse
|
||||
|
||||
with open(settings.CONTRIBUTORS_DATA) as f:
|
||||
data = json.load(f)
|
||||
|
||||
return render_to_response(
|
||||
'zerver/authors.html',
|
||||
data,
|
||||
request=request
|
||||
)
|
||||
|
|
|
@ -1103,3 +1103,5 @@ if PRODUCTION:
|
|||
PROFILE_ALL_REQUESTS = False
|
||||
|
||||
CROSS_REALM_BOT_EMAILS = set(('feedback@zulip.com', 'notification-bot@zulip.com'))
|
||||
|
||||
CONTRIBUTORS_DATA = os.path.join(STATIC_ROOT, 'generated/github-contributors.json')
|
||||
|
|
|
@ -132,11 +132,11 @@ i18n_urls = [
|
|||
url(r'^robots\.txt$', RedirectView.as_view(url='/static/robots.txt', permanent=True)),
|
||||
|
||||
# Landing page, features pages, signup form, etc.
|
||||
url(r'^hello/$', TemplateView.as_view(template_name='zerver/hello.html'),
|
||||
name='landing-page'),
|
||||
url(r'^hello/$', TemplateView.as_view(template_name='zerver/hello.html'), name='landing-page'),
|
||||
url(r'^new-user/$', RedirectView.as_view(url='/hello', permanent=True)),
|
||||
url(r'^features/$', TemplateView.as_view(template_name='zerver/features.html')),
|
||||
url(r'^find_my_team/$', zerver.views.registration.find_my_team, name='zerver.views.registration.find_my_team'),
|
||||
url(r'^authors/$', zerver.views.users.authors_view, name='zerver.views.users.authors_view')
|
||||
]
|
||||
|
||||
# If a Terms of Service is supplied, add that route
|
||||
|
|
Loading…
Reference in New Issue