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
|
/zproject/dev-secrets.conf
|
||||||
static/js/bundle.js
|
static/js/bundle.js
|
||||||
static/generated/emoji
|
static/generated/emoji
|
||||||
|
static/generated/github-contributors.json
|
||||||
static/locale/language_options.json
|
static/locale/language_options.json
|
||||||
/node_modules
|
/node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
|
@ -503,6 +503,48 @@ a.bottom-signup-button {
|
||||||
font-size: 25px;
|
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 {
|
.integration-lozenges {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -568,12 +610,13 @@ a.bottom-signup-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page-header,
|
.api-page-header,
|
||||||
.apps-page-header,
|
.apps-page-header,
|
||||||
|
.authors-page-header,
|
||||||
.feature-page-header,
|
.feature-page-header,
|
||||||
.integrations-page-header,
|
.integrations-page-header,
|
||||||
.register-page-header,
|
.login-page-header,
|
||||||
.api-page-header {
|
.register-page-header {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -594,11 +637,11 @@ a.bottom-signup-button {
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-page-header,
|
|
||||||
.api-page-header,
|
.api-page-header,
|
||||||
.apps-page-header,
|
.apps-page-header,
|
||||||
.integrations-page-header
|
.authors-page-header,
|
||||||
{
|
.feature-page-header,
|
||||||
|
.integrations-page-header {
|
||||||
padding-top: 40px;
|
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(["sudo", "chown", "%s:%s" % (user_id, user_id), EMOJI_CACHE_PATH])
|
||||||
run(["tools/setup/emoji/build_emoji"])
|
run(["tools/setup/emoji/build_emoji"])
|
||||||
run(["scripts/setup/generate_secrets.py", "--development"])
|
run(["scripts/setup/generate_secrets.py", "--development"])
|
||||||
|
run(["tools/update-authors-json", "--use-fixture"])
|
||||||
if options.is_travis and not options.is_production_travis:
|
if options.is_travis and not options.is_production_travis:
|
||||||
run(["sudo", "service", "rabbitmq-server", "restart"])
|
run(["sudo", "service", "rabbitmq-server", "restart"])
|
||||||
run(["sudo", "service", "redis-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')],
|
os.path.join(settings.STATIC_ROOT, 'locale')],
|
||||||
stdout=fp, stderr=fp)
|
stdout=fp, stderr=fp)
|
||||||
|
|
||||||
|
# Generate /authors page markdown
|
||||||
|
subprocess.check_call(['./tools/update-authors-json'], stdout=fp)
|
||||||
|
|
||||||
fp.close()
|
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 heapq
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpRequest
|
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
|
from zerver.lib.str_utils import force_text
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
@ -204,3 +205,12 @@ def check_subdomain(realm_subdomain, user_subdomain):
|
||||||
if realm_subdomain != user_subdomain:
|
if realm_subdomain != user_subdomain:
|
||||||
return False
|
return False
|
||||||
return True
|
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.lib.session_user import get_session_dict_user
|
||||||
from zerver.middleware import is_slow_query
|
from zerver.middleware import is_slow_query
|
||||||
from zerver.lib.avatar import avatar_url
|
from zerver.lib.avatar import avatar_url
|
||||||
|
from zerver.lib.utils import split_by
|
||||||
|
|
||||||
from zerver.worker import queue_processors
|
from zerver.worker import queue_processors
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ import time
|
||||||
import ujson
|
import ujson
|
||||||
import random
|
import random
|
||||||
import filecmp
|
import filecmp
|
||||||
|
import subprocess
|
||||||
|
|
||||||
def bail(msg):
|
def bail(msg):
|
||||||
# type: (str) -> None
|
# type: (str) -> None
|
||||||
|
@ -2129,6 +2131,28 @@ class HomeTest(ZulipTestCase):
|
||||||
result = self.client_get("/api/v1/generate_204")
|
result = self.client_get("/api/v1/generate_204")
|
||||||
self.assertEqual(result.status_code, 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):
|
class MutedTopicsTests(ZulipTestCase):
|
||||||
def test_json_set(self):
|
def test_json_set(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
|
@ -2337,3 +2361,10 @@ class TestFindMyTeam(ZulipTestCase):
|
||||||
result = self.client_post('/find_my_team/', data)
|
result = self.client_post('/find_my_team/', data)
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assertIn("Please enter at most 10", result.content.decode('utf8'))
|
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 __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.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.conf import settings
|
||||||
from six.moves import map
|
from six.moves import map
|
||||||
|
|
||||||
from zerver.decorator import has_request_variables, REQ, JsonableError, \
|
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.lib.utils import generate_random_token
|
||||||
from zerver.models import UserProfile, Stream, Realm, Message, get_user_profile_by_email, \
|
from zerver.models import UserProfile, Stream, Realm, Message, get_user_profile_by_email, \
|
||||||
get_stream, email_allowed_for_realm
|
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):
|
def deactivate_user_backend(request, user_profile, email):
|
||||||
# type: (HttpRequest, UserProfile, Text) -> HttpResponse
|
# type: (HttpRequest, UserProfile, Text) -> HttpResponse
|
||||||
|
@ -361,3 +365,15 @@ def get_profile_backend(request, user_profile):
|
||||||
result['max_message_id'] = messages[0].id
|
result['max_message_id'] = messages[0].id
|
||||||
|
|
||||||
return json_success(result)
|
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
|
PROFILE_ALL_REQUESTS = False
|
||||||
|
|
||||||
CROSS_REALM_BOT_EMAILS = set(('feedback@zulip.com', 'notification-bot@zulip.com'))
|
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)),
|
url(r'^robots\.txt$', RedirectView.as_view(url='/static/robots.txt', permanent=True)),
|
||||||
|
|
||||||
# Landing page, features pages, signup form, etc.
|
# Landing page, features pages, signup form, etc.
|
||||||
url(r'^hello/$', TemplateView.as_view(template_name='zerver/hello.html'),
|
url(r'^hello/$', TemplateView.as_view(template_name='zerver/hello.html'), name='landing-page'),
|
||||||
name='landing-page'),
|
|
||||||
url(r'^new-user/$', RedirectView.as_view(url='/hello', permanent=True)),
|
url(r'^new-user/$', RedirectView.as_view(url='/hello', permanent=True)),
|
||||||
url(r'^features/$', TemplateView.as_view(template_name='zerver/features.html')),
|
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'^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
|
# If a Terms of Service is supplied, add that route
|
||||||
|
|
Loading…
Reference in New Issue