Port the pullrequest dashboard as well

This commit is contained in:
Alexandre Aubin 2018-01-28 21:39:04 -05:00
parent 7e14e5860f
commit 6970387bad
13 changed files with 337 additions and 55 deletions

View file

@ -1,5 +1,6 @@
from flask import render_template, Blueprint
from .models import App, AppCI, AppCIBranch
from .models.pr import PullRequest
from .models.appci import App, AppCI, AppCIBranch
from .settings import SITE_ROOT
main = Blueprint('main', __name__, url_prefix=SITE_ROOT)
@ -21,6 +22,23 @@ def index():
return render_template('index.html')
@main.route('/pullrequests')
def pullrequests():
prs = PullRequest.query.all()
prs = sorted(prs, key=lambda pr: (pr.review_priority, pr.created), reverse=True)
active_prs = [ pr for pr in prs if pr.review_priority >= 0]
count_by_team = { "all": len(active_prs),
"core": len([pr for pr in active_prs if pr.repo.team == "core"]),
"apps": len([pr for pr in active_prs if pr.repo.team == "apps"]),
"infra": len([pr for pr in active_prs if pr.repo.team == "infra"]),
"doc": len([pr for pr in active_prs if pr.repo.team == "doc"]) }
return render_template("pullrequests.html", prs=prs, count_by_team=count_by_team)
@main.route('/appci/branch/<branch>')
def appci_branch(branch):

1
app/models/__init__.py Normal file
View file

@ -0,0 +1 @@
from . import appci, pr

View file

@ -1,12 +1,10 @@
import os
import time
import json
import requests
import dateutil.parser
from flask_sqlalchemy import SQLAlchemy
from .. import db
from . import db
class AppList(db.Model):
@ -293,7 +291,7 @@ class Github():
def __init__(self):
from .settings import GITHUB_USER, GITHUB_TOKEN
from ..settings import GITHUB_USER, GITHUB_TOKEN
self.user = GITHUB_USER
self.token = GITHUB_TOKEN

133
app/models/pr.py Normal file
View file

@ -0,0 +1,133 @@
import json
import requests
import datetime
from .. import db
class Repo(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False)
team = db.Column(db.String(64), nullable=False)
def __repr__(self):
return '<Repo %r>' % self.name
def init():
d = {
"core": ["yunohost",
"yunohost-admin",
"SSOwat",
"moulinette",
"Vagrantfile",
"ynh-dev"],
"doc": ["doc",
"Simone",
"project-organization"],
"apps": ["apps",
"CI_package_check",
"example_ynh",
"package_linter",
"package_check"],
"infra": ["build.yunohost.org",
"dynette",
"YunoPorts",
"cd_build",
"install_script",
"trotinette",
"bicyclette",
"install-app",
"tartiflette",
"vinaigrette"]
}
for team, repos in d.items():
for repo in repos:
yield Repo(name=repo, team=team)
def update(self):
print("Updating PRs for repo %s ..." % self.name)
PullRequest.query.filter_by(repo=self).delete()
issues = requests.get("https://api.github.com/repos/yunohost/%s/issues?per_page=100" % self.name)
issues = json.loads(issues.text)
issues = [i for i in issues if "pull_request" in i.keys()]
for issue in issues:
print(" > Analyzing %s-%s" % (self.name, issue["number"]))
db.session.add(PullRequest(self, issue))
db.session.commit()
class PullRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
id_ = db.Column(db.String(64), unique=True, nullable=False)
title = db.Column(db.String(256), nullable=False)
labels = db.Column(db.PickleType)
url = db.Column(db.String(128), nullable=False)
milestone = db.Column(db.String(32), nullable=False)
review_priority = db.Column(db.Integer)
created = db.Column(db.DateTime)
updated = db.Column(db.DateTime)
repo = db.relationship(Repo, backref='prs', lazy=True, uselist=False)
repo_id = db.Column(db.ForeignKey(Repo.id))
def __init__(self, repo, issue):
self.repo = repo
self.title = issue["title"]
self.labels = [label["name"] for label in issue["labels"]]
self.milestone = issue["milestone"]["title"] if issue["milestone"] else ""
self.id_ = "%s-%s" % (repo.name, issue["number"])
self.created = datetime.datetime.strptime(issue["created_at"], "%Y-%m-%dT%H:%M:%SZ")
self.updated = datetime.datetime.strptime(issue["updated_at"], "%Y-%m-%dT%H:%M:%SZ")
self.url = issue["pull_request"]["html_url"]
for size in ["small", "medium", "big"]:
if "%s decision" % size in self.labels:
self.labels.remove("%s decision" % size)
self.labels.insert(0, size)
now = datetime.datetime.now()
if (now - self.created).days > 60 and (now - self.updated).days > 30:
self.labels.append("dying")
self.review_priority = self.get_review_priority()
def get_review_priority(self):
if "important" in self.labels:
base_priority = 100
elif "opinion needed" in self.labels:
base_priority = 50
elif "postponed" in self.labels or "inactive" in self.labels:
base_priority = -100
else:
base_priority = 0
if "work needed" in self.labels:
base_priority += -5
if "dying" in self.labels and base_priority > -100:
base_priority += 5
return base_priority
def init():
pass

File diff suppressed because one or more lines are too long

View file

@ -8,15 +8,11 @@
margin-top:4em;
}
#app-ci-test-results
{
margin-left:auto;
margin-right:auto;
/*width:1000px;*/
}
a { color:#007bff; }
table, thead, tbody { display: block; width: 100%;}
table.ci-app { margin: 0 auto; margin-top: 100px; max-width: 1070px; overflow-x: visible; }
.ci-app-table > thead, .ci-app-table > tbody { display: block; width: 100%;}
.ci-app-table { margin: 0 auto; margin-top: 100px; max-width: 1070px; overflow-x: visible; }
.ci-app-table th, .ci-app-table td { display: block; border: none; padding; 0px;float: left; height:33px; width: 33px; margin: 5px; }
th.ci-app-test-title
{
@ -40,24 +36,32 @@ th.ci-app-test-title > div > span
border:none;
}
th, td { display: block; border: none; padding; 0px;float: left; height:33px; width: 33px; margin: 5px; }
.ci-app-row-title { text-align: center; width:150px; }
td.ci-app-test-result { text-align:center; padding: 14px 8px 8px 8px; }
td.ci-app-test-result > div { position:relative; background-color: #bdc3c7; border-radius:5px; width: 18px; height: 18px;}
td.ci-app-test-result > div.success { background-color: rgb(46,204,83); }
td.ci-app-test-result > div.danger { background-color: rgb(225,80,62); }
.table > thead > tr > th { border : none; }
.table > tbody > tr > td { border : none; }
.ci-app-table > thead > tr > th { border : none; }
.ci-app-table > tbody > tr > td { border : none; }
.ci-app-table .ci-app-row-title { text-align: center; width:150px; }
.ci-app-table .ci-app-test-info { width:150px; font-size: 12px; }
.canvasjs-chart-credit { display: none; }
.ci-app-test-info { width:150px; font-size: 12px; }
.daysAgo a { color: #777; }
.official-star { color: #f0ad4e; }
.level-improvement { color: #2d2; }
.level-regression { color: #f22; }
.level-unknown { color: #aaa; }
.text-warning { color: #f0ad0e !important; }
.table th { border-top: none; }
.table-pullrequests { width: 1250px; font-family: sans-serif; border: none; }
.column-pr-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width:400px;
}

View file

@ -2,21 +2,18 @@
function daysAgo(timestamp) {
var difference = Math.round(+new Date()/1000) - timestamp;
var daysDifference = Math.round(difference/60/60/24);
//return (new Date(timestamp*1000));
return "(" + daysDifference + " days ago)";
}
$(".daysAgo").each(function () {
var t = $(this).attr("timestamp");
var console = $(this).attr("console");
var href = $(this).attr("href");
if (t) {
link = document.createElement('a');
link.setAttribute('href', console);
link.setAttribute('href', href);
link.innerHTML = daysAgo(t);
this.appendChild(link);
}
// $(this).text(daysAgo(t));
//}
});
$(".ci-app-test-result div[value=True]").addClass("success")

View file

@ -1,15 +1,15 @@
{% extends "base.html" %}
{% block content %}
<h2 class="text-center">{{ app.name.title() }}</h2>
<h2 class="text-center my-3">{{ app.name.title() }}</h2>
<div class="row">
<div id="app-ci-test-results">
<div class="mx-auto">
<div>
<table class="table table-responsive ci-app">
<table class="table table-responsive ci-app-table">
<thead>
<tr>
<th class="ci-app-row-title"><div></div></th>
<th class="ci-app-row-title"></th>
<th class="ci-app-test-title"><div>Level</div></th>
{% for test in tests %}
<th class="ci-app-test-title"><div><span>{{ test }}</span></div></th>
@ -36,9 +36,9 @@
{% endfor %}
<td class="ci-app-test-info">
{% if result.date == None %}
<span class="daysAgo" console="{{ result.url }}">???</span>
<span class="daysAgo" href="{{ result.url }}">???</span>
{% else %}
<span class="daysAgo" timestamp="{{ result.date.timestamp() }}" console="{{ result.url }}"></span>
<span class="daysAgo" timestamp="{{ result.date.timestamp() }}" href="{{ result.url }}"></span>
{% endif %}
{% if result.commit != result.app.master_commit %}
<span class="oi oi-clock text-warning"

View file

@ -1,16 +1,16 @@
{% extends "base.html" %}
{% block content %}
<h2 class="text-center">{{ branch.display_name }}</h2>
<div id="levelSummary" style="height: 270px;" class="col-sm-6 offset-sm-3"></div>
<h2 class="text-center my-3">{{ branch.display_name }}</h2>
<div id="levelSummary" style="height: 270px;" class="col-sm-6 offset-sm-3 my-3"></div>
<div class="row">
<div id="app-ci-test-results">
<div class="mx-auto">
<div>
<table class="table table-responsive ci-app">
<table class="table table-responsive ci-app-table">
<thead>
<tr>
<th class="ci-app-row-title"><div></div></th>
<th class="ci-app-row-title"></th>
<th class="ci-app-test-title"><div>Level</div></th>
{% for test in tests %}
<th class="ci-app-test-title"><div><span>{{ test }}</span></div></th>
@ -40,9 +40,9 @@
{% endfor %}
<td class="ci-app-test-info">
{% if result.date == None %}
<span class="daysAgo" console="{{ result.url }}">???</span>
<span class="daysAgo" href="{{ result.url }}">???</span>
{% else %}
<span class="daysAgo" timestamp="{{ result.date.timestamp() }}" console="{{ result.url }}"></span>
<span class="daysAgo" timestamp="{{ result.date.timestamp() }}" href="{{ result.url }}"></span>
{% endif %}
{% if result.commit != result.app.master_commit %}
<span class="oi oi-clock text-warning"

View file

@ -1,15 +1,15 @@
{% extends "base.html" %}
{% block content %}
<h2 class="text-center">{{ ref.display_name }} vs. {{ target.display_name }}</h2>
<h2 class="text-center my-3">{{ ref.display_name }} vs. {{ target.display_name }}</h2>
<div class="row">
<div id="app-ci-test-results">
<div class="mx-auto">
<div>
<table class="table table-responsive ci-app">
<table class="table table-responsive ci-app-table">
<thead>
<tr>
<th class="ci-app-row-title"><div></div></th>
<th class="ci-app-row-title"></th>
<th class="ci-app-test-title"><div>{{ ref.display_name }} </div></th>
<th class="ci-app-test-title"><div>{{ target.display_name }} </div></th>
<th class="ci-app-test-title"></th>

View file

@ -13,7 +13,7 @@
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#">YunoHost Dev Dashboard</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarColor01">

View file

@ -0,0 +1,107 @@
{% extends "base.html" %}
{% block content %}
<h3 class="text-center my-5">What should you review today ?</h3>
<div class="row">
<div class="mx-auto mb-5">
<ul id="select-team" class="nav nav-pills">
<li class="nav-item">
<a id="select-team-all" class="nav-link active" href="javascript:void(0)" onclick="filter('')">All <span class="badge badge-secondary badge-pill">{{ count_by_team["all"] }}</span></a>
</li>
<li class="nav-item"><a id="select-team-core" class="nav-link" href="javascript:void(0)" onclick="filter('team-core')">Core <span class="badge badge-secondary badge-pill">{{ count_by_team["core"] }}</span></a></li>
<li class="nav-item"><a id="select-team-apps" class="nav-link" href="javascript:void(0)" onclick="filter('team-apps')">Apps <span class="badge badge-secondary badge-pill">{{ count_by_team["apps"] }}</span></a/li>
<li class="nav-item"><a id="select-team-infra" class="nav-link" href="javascript:void(0)" onclick="filter('team-infra')">Infra / dist <span class="badge badge-secondary badge-pill">{{ count_by_team["infra"] }}</span></a></li>
<li class="nav-item"><a id="select-team-doc" class="nav-link" href="javascript:void(0)" onclick="filter('team-doc')">Doc <span class="badge badge-secondary badge-pill">{{ count_by_team["doc"] }}</span></a></li>
</ul>
</div>
</div>
<div class="row">
<div class="mx-auto">
<table id="pullrequests" class="table table-responsive table-sm table-pullrequests">
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Created</th>
<th>Labels</th>
</tr>
</thead>
<tbody>
{% for pr in prs %}
<tr class="team-{{ pr.repo.team }}">
<td class="col-md-2 text-center">
<a class="btn btn-sm mx-4 py-2 px-3
{% if pr.review_priority >= 90 %}btn-warning{% else %}
{% if pr.review_priority >= 40 %}btn-success{% else %}
{% if pr.review_priority >= -10 %}btn-info{% else %}
{% if pr.review_priority >= -60 %}btn-secondary{% else %}
btn-link{% endif %}
{% endif %}
{% endif %}
{% endif %} text-uppercase font-weight-bold"
href="{{ pr.url }}">{{ pr.id_ }}</a>
</td>
<td class="column-pr-title font-weight-bold"><strong>{{ pr.title }}</strong></td>
<td class="col-md-1 daysAgo" timestamp="{{ pr.created.timestamp() }}"
style="font-size: 12px;" ></td>
<td class="col-md-4">
{% for label in pr.labels %}
<span class="badge ml-1
{% if label == "important" %}badge-danger{%else%}
{% if label == "opinion needed" %}badge-warning{%else%}
{% if label == "small" %}badge-info{%else%}
{% if label == "medium" %}badge-info{%else%}
{% if label == "big" %}badge-info{%else%}
{% if label == "ready to merge" %}badge-success{%else%}
{% if label == "work needed" %}badge-primary{%else%}
{% if label == "inactive" %}badge-secondary{%else%}
{% if label == "postponed" %}badge-secondary{%else%}
{% if label == "dying" %}badge-danger{%else%}
badge-secondary
{%endif%}{%endif%}{%endif%}{%endif%}{%endif%}{%endif%}{%endif%}{%endif%}{%endif%}{%endif%}
">{{ label }}</span>
{% endfor %}
</td>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
function filter(team) {
// Declare variables
var input, filter, table, tr, td, i;
table = document.getElementById("pullrequests");
tr = table.getElementsByTagName("tr");
// Loop through all table rows, and hide those who don't match the search query
for (i = 0; i < tr.length; i++)
{
if (team == '') { tr[i].style.display = ""; }
else if (tr[i].classList == "") { tr[i].style.display = ""; }
else if (tr[i].classList.contains(team)) { tr[i].style.display = ""; }
else { tr[i].style.display = "none"; }
}
selector = document.getElementById("select-team");
a = selector.getElementsByTagName("a");
if (team == "") { team = "team-all"; }
for (i = 0; i < a.length; i++)
{
if (a[i].getAttribute("id") == "select-".concat(team))
{
a[i].classList.add("active");
}
else
{
a[i].classList.remove("active");
}
}
}
</script>
{% endblock %}

View file

@ -5,23 +5,34 @@ from app import db, create_app
app = create_app()
def main():
manager = Manager(app)
manager.add_command('shell', Shell(make_context=lambda:{"app":app, "db":db}))
manager.add_command('nuke', Nuke(db))
manager.add_command('init', Init(db))
manager.add_command('update', Update(db))
manager.add_command('update-appci', Update(db, "appci"))
manager.add_command('update-pr', Update(db, "pr"))
manager.run()
class Update(Command):
def __init__(self, db):
def __init__(self, db, what):
self.db = db
self.what = what
def run(self):
from app.models import AppCI
AppCI.update()
if self.what == "appci":
from app.models.appci import AppCI
AppCI.update()
elif self.what == "pr":
from app.models.pr import Repo
for repo in Repo.query.all():
repo.update()
else:
pass
class Nuke(Command):
@ -32,7 +43,9 @@ class Nuke(Command):
def run(self):
import app.models
import app.models.appci
import app.models.pr
print("> Droping tables...")
self.db.drop_all()
print("> Creating tables...")
@ -40,6 +53,7 @@ class Nuke(Command):
print("> Comitting sessions...")
self.db.session.commit()
class Init(Command):
def __init__(self, db):
@ -48,8 +62,12 @@ class Init(Command):
def run(self):
import app.models
stuff_in_module = [ app.models.__dict__.get(s) for s in dir(app.models) ]
models = [ m for m in stuff_in_module if isinstance(m, type(db.Model)) ]
# Black magic to extract list of models from 'models' folder
submodules = [ app.models.__dict__.get(m) for m in dir(app.models) if not m.startswith('__') ]
stuff = []
for submodule in submodules:
stuff.extend([submodule.__dict__.get(s) for s in dir(submodule)])
models = [s for s in stuff if isinstance(s, type(db.Model))]
for model in models:
objs = model.init()
@ -59,5 +77,6 @@ class Init(Command):
db.session.add(obj)
db.session.commit()
if __name__ == '__main__':
main()