├── .gitignore ├── Dockerfile ├── Procfile ├── README.md ├── application.py ├── auth.py ├── config.py ├── github.py ├── plugins ├── __init__.py └── abiquo.py ├── requirements.txt ├── static ├── images │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon.png │ ├── dropdown.png │ ├── favicon.ico │ └── octocat.jpg └── stylesheets │ ├── base.css │ ├── basic.css │ ├── layout.css │ └── skeleton.css └── templates ├── abiquo.html ├── columns.html ├── filters.html ├── layout.html └── signin.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .ropeproject 37 | .ruby-version 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-onbuild 2 | MAINTAINER Ignasi Barrera 3 | 4 | EXPOSE 8080 5 | CMD ["python", "application.py"] 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn application:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Code review dashboard 2 | ===================== 3 | 4 | A dashboard to see the status of all opened pull requests. It is configurable and extensible so you can customize the information that is shown for each pull request. The `basic` template provided as an example shows the pull requests classified in three columns: 5 | 6 | * **Cold** pull requests are the ones where no one has given a +1. 7 | * **Hot!** ones are those that are waiting for the last +1 to be merged. 8 | * **Burning** pull requests are those that are ready to be merged. 9 | 10 | It also shows in red the pull requests that have been without activity in the configured days, and shows in green the pull requests where the current user has participated. 11 | 12 | Configuration 13 | ------------- 14 | 15 | The dashboard is configured in the `config.py` file. Feel free to edit and adapt it to your needs. 16 | 17 | Running as a Docker container (recommended) 18 | ------------------------------------------- 19 | 20 | If you prefer to run the dashboard as a Docker container, you just have to build the image and 21 | run the container as follows: 22 | 23 | # Build the Docker image (only the first time) 24 | docker build -t abiquo/code-review-dashboard . 25 | 26 | # Run the container 27 | docker run -d -p 80:8080 \ 28 | -e CLIENT_ID= \ 29 | -e CLIENT_SECRET= \ 30 | -e SECRET_KEY= \ 31 | abiquo/code-review-dashboard 32 | 33 | Running without Docker 34 | ---------------------- 35 | 36 | The dashboard uses [Flask](http://flask.pocoo.org/docs/) and [Requests](http://python-requests.org). 37 | You can install them using [Pip](http://www.pip-installer.org) as follows: 38 | 39 | pip install Flask requests 40 | 41 | If you don't have *pip* installed, you can install it following the instructions found in the site. It can 42 | be installed in a virtualenv or in the core system. Here is how you can install it in your system. Installing 43 | it into a virtualenv should be the same, once it has been activated: 44 | 45 | # Install setuptools 46 | wget https://pypi.python.org/packages/2.7/s/setuptools/setuptools-0.6c11-py2.7.egg#md5=fe1f997bc722265116870bc7919059ea 47 | sudo sh sh setuptools-0.6c9-py2.4.egg 48 | 49 | # Install pip 50 | curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py 51 | sudo python get-pip.py 52 | 53 | Once you have installed the requirements you can run the dashboard as follows: 54 | 55 | python application.py 56 | 57 | Deploying to Heroku 58 | ------------------- 59 | 60 | The application can also be deployed to Heroku. To deploy it you just have to create the application 61 | and deploy it as follows: 62 | 63 | # Create and configure the Heroku application 64 | heroku create 65 | 66 | # Deploy the application 67 | git push heroku master 68 | 69 | # Set application ID and secret as heroku envs 70 | heroku config:set CLIENT_ID=ID 71 | heroku config:set CLIENT_SECRET=secret 72 | 73 | # Set a flask application secret key 74 | heroku config:set SECRET_KEY=secret-key 75 | -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | from auth import requires_auth 2 | from flask import Flask, render_template, request, url_for, redirect, session 3 | from github import Github 4 | from auth import Token 5 | import config 6 | import timeit 7 | import os 8 | import requests 9 | import string 10 | import subprocess 11 | import random 12 | 13 | 14 | app = Flask(__name__) 15 | app.secret_key = os.environ.get('SECRET_KEY') or config.SECRET_KEY 16 | 17 | 18 | def load_plugin(name): 19 | module = __import__("plugins." + name, fromlist=["plugins"]) 20 | plugin = module.load() 21 | plugin_config = plugin.config 22 | for prop in plugin_config.iterkeys(): 23 | setattr(config, prop, plugin_config[prop]) 24 | return plugin, plugin_config 25 | 26 | 27 | @app.route("/signin") 28 | @requires_auth 29 | def signin(auth=None): 30 | if not auth: 31 | return render_template('signin.html') 32 | else: 33 | return redirect(url_for('index')) 34 | 35 | 36 | @app.route("/") 37 | @requires_auth 38 | def index(auth=None): 39 | if not auth: 40 | return redirect(url_for('signin')) 41 | 42 | plugin, plugin_config = load_plugin(config.PLUGIN) 43 | github = Github(auth, plugin) 44 | 45 | start = timeit.default_timer() 46 | result = github.search_pulls() 47 | end = timeit.default_timer() 48 | 49 | summaries = {'left': [], 'middle': [], 'right': []} 50 | authors = set() 51 | branches = set() 52 | repos = set() 53 | for pull in result['pulls']: 54 | summaries[plugin.classify(pull)].append(pull) 55 | authors.add(pull['author']) 56 | branches.add(pull['target_branch']) 57 | repos.add(pull['repo_name']) 58 | 59 | session['token'] = {'token': github.credentials.token, 60 | 'user': github.credentials.user, 61 | 'name': github.credentials.name} 62 | 63 | stats = { 64 | 'threads': result['total-threads'], 65 | 'requests': result['total-requests'], 66 | 'rate-limit': result['rate-limit'], 67 | 'process-time': end - start, 68 | 'total-left': len(summaries['left']), 69 | 'total-middle': len(summaries['middle']), 70 | 'total-right': len(summaries['right']), 71 | 'git-commit': git_commit 72 | } 73 | 74 | return render_template('columns.html', 75 | title=plugin_config['title'], 76 | headers=plugin_config['headers'], 77 | template=plugin_config['template'], 78 | stats=stats, 79 | pulls=summaries, 80 | authors=sorted(authors), 81 | branches=sorted(branches), 82 | repos=sorted(repos), 83 | user=github.user()) 84 | 85 | 86 | @app.route('/login') 87 | def authenticate(): 88 | """ According to specification the 'state' must be unpredictable """ 89 | scope = request.args.get('scope') 90 | if scope: 91 | scope = 'repo' 92 | 93 | oauth_state = ''.join(random.choice(string.ascii_uppercase + 94 | string.digits) for x in range(32)) 95 | client_id = os.environ.get('CLIENT_ID') or config.CLIENT_ID 96 | uri = 'https://github.com/login/oauth/authorize?client_id=' +\ 97 | client_id + '&state=' + oauth_state 98 | if scope: 99 | uri = uri + '&scope=' + scope 100 | return redirect(uri) 101 | 102 | 103 | @app.route("/authorize") 104 | def authorize(): 105 | try: 106 | if request.args: 107 | code = request.args.get('code') 108 | token = __request_token(code) 109 | if token: 110 | session['token'] = {'token': token.token} 111 | 112 | except Exception, e: 113 | print "Error ", e 114 | 115 | return redirect(url_for('index')) 116 | 117 | 118 | @app.route('/logout') 119 | def logout(): 120 | if 'token' in session: 121 | session.pop('token') 122 | return redirect(url_for('index')) 123 | 124 | 125 | def __request_token(code): 126 | 127 | client_id = os.environ.get('CLIENT_ID') or config.CLIENT_ID 128 | client_secret = os.environ.get('CLIENT_SECRET') or config.CLIENT_SECRET 129 | uri = 'https://github.com/login/oauth/access_token?client_id=' +\ 130 | client_id + '&client_secret=' + client_secret + '&code=' + code 131 | 132 | header = {'Content-type': 'application/json'} 133 | r = requests.post(uri, 134 | headers=header) 135 | d = dict(s.split('=') for s in r.content.split('&')) 136 | if 'error' in d: 137 | return None 138 | 139 | if 'access_token' in d: 140 | return Token(d['access_token']) 141 | 142 | return Token('') 143 | 144 | 145 | def __gitcommit(): 146 | cmd = ["git", "log", "-1", "--pretty=format:%h"] 147 | return subprocess.check_output(cmd).strip() 148 | 149 | 150 | git_commit = __gitcommit() 151 | 152 | 153 | if __name__ == "__main__": 154 | app.debug = True 155 | app.run(host=config.LISTEN_ADDRESS, port=config.LISTEN_PORT) 156 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | from functools import wraps 3 | 4 | 5 | class Token(object): 6 | def __init__(self, token): 7 | self.token = token 8 | self.user = '' 9 | self.name = '' 10 | 11 | 12 | def requires_auth(f): 13 | @wraps(f) 14 | def decorated(*args, **kwargs): 15 | auth = None 16 | if 'token' in session: 17 | temp = session['token'] 18 | auth = Token(temp.get('token', None)) 19 | auth.user = temp.get('user', None) 20 | auth.name = temp.get('name', None) 21 | 22 | kwargs['auth'] = auth 23 | return f(*args, **kwargs) 24 | return decorated 25 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | ################# 2 | # Configuration # 3 | ################# 4 | 5 | LISTEN_ADDRESS = '0.0.0.0' # Address where the application listens 6 | LISTEN_PORT = 8080 # Port where the application listens 7 | PLUGIN = 'abiquo' # The plugin to load 8 | DEBUG = True # Print debut information 9 | THREADED = True # Use multiple threads to perform the Github API calls 10 | MAX_RETRIES = 5 # Number of retries for failed requests 11 | DELAY = 1 # The default delay (in seconds) to wait before retrying failed requests 12 | BACKOFF = 2 # The backoff factor to wait between retries 13 | CLIENT_ID = 'CHANGE_ME' # Github client ID 14 | CLIENT_SECRET = 'CHANGE_ME' # Github client secret 15 | SECRET_KEY = 'CHANGE_ME' # Flask app secret key (to decrypt cookies, better shared across all instances) 16 | -------------------------------------------------------------------------------- /github.py: -------------------------------------------------------------------------------- 1 | import config 2 | import datetime 3 | import Queue 4 | import requests 5 | import threading 6 | import time 7 | import traceback 8 | 9 | from collections import defaultdict 10 | 11 | class Github: 12 | def __init__(self, credentials, plugin): 13 | self.credentials = credentials 14 | self.total_threads = 0 15 | self.total_requests = 0 16 | self.remaining_rl = None 17 | self.plugin = plugin 18 | self.plugin.github = self 19 | self.check_unicodes = { 20 | 'queued': '9685', 21 | 'in_progress': '9685', 22 | 'action_required': '65794', 23 | 'canceled': '65794', 24 | 'timed_out': '65794', 25 | 'failed': '65794', 26 | 'neutral': '9685', 27 | 'success': '10003', 28 | 'failure': '65794' 29 | } 30 | 31 | def user(self): 32 | if not self.credentials.user: 33 | self.__request_user() 34 | return self.credentials.user 35 | 36 | def name(self): 37 | if not self.credentials.name: 38 | self.__request_user() 39 | return self.credentials.name 40 | 41 | def list_org_repos(self, org): 42 | url = 'https://api.github.com/orgs/%s/repos' % org 43 | return [repo['url'] for repo in self.get(url)] 44 | 45 | def list_user_repos(self, user): 46 | url = 'https://api.github.com/users/%s/repos' % user 47 | return [repo['url'] for repo in self.get(url)] 48 | 49 | def search_pulls(self): 50 | threads = [] 51 | results = Queue.Queue() 52 | 53 | if config.THREADED: 54 | for repo_url in self.plugin.repos: 55 | try: 56 | repo = self.get(repo_url) 57 | t = threading.Thread(target=self._analyze_repo, 58 | args=(repo, results,)) 59 | threads.append(t) 60 | t.start() 61 | except: 62 | print "Could not analyze repository: %s" % repo_url 63 | traceback.print_exc() 64 | self.__incr_threads(len(threads)) 65 | for thread in threads: 66 | thread.join() 67 | else: 68 | for repo_url in self.plugin.repos: 69 | try: 70 | repo = self.get(repo_url) 71 | self._analyze_repo(repo, results) 72 | except: 73 | print "Could not analyze repository: %s" % repo_url 74 | traceback.print_exc() 75 | 76 | pulls = [] 77 | while not results.empty(): 78 | pulls.append(results.get()) 79 | 80 | return { 81 | "pulls": pulls, 82 | "total-threads": self.total_threads, 83 | "total-requests": self.total_requests, 84 | "rate-limit": self.remaining_rl 85 | } 86 | 87 | def _analyze_repo(self, repo, results): 88 | payload = {"state": "open"} 89 | pulls = self.get(repo["pulls_url"].replace("{/number}", ""), payload) 90 | threads = [] 91 | 92 | if config.THREADED: 93 | for pull_head in pulls: 94 | try: 95 | t = threading.Thread(target=self._analyze_pull, 96 | args=(repo, pull_head, results,)) 97 | threads.append(t) 98 | t.start() 99 | except: 100 | if config.DEBUG: 101 | print "Could not analyze pull request: %s" % pull_head["url"] 102 | self.__incr_threads(len(threads)) 103 | for thread in threads: 104 | thread.join() 105 | else: 106 | for pull_head in pulls: 107 | try: 108 | self._analyze_pull(repo, pull_head, results) 109 | except: 110 | if config.DEBUG: 111 | print "Could not analyze pull request: %s" % pull_head["url"] 112 | 113 | return pulls 114 | 115 | def _analyze_pull(self, repo, pull_head, results): 116 | pull = self.get(pull_head["url"]) 117 | summary = {} 118 | summary['name'] = pull["title"] 119 | summary['url'] = pull["html_url"] 120 | summary['repo_name'] = repo["name"] 121 | summary['repo_url'] = repo["html_url"] 122 | summary['author'] = pull["user"]["login"] 123 | summary['old'] = self.get_days_old(pull) 124 | summary['target_branch'] = pull["base"]["ref"] 125 | summary['build_status'] = self._get_build_status(pull) 126 | summary['likes'] = 0 127 | summary['dislikes'] = 0 128 | summary['checks'] = self._get_check_status(pull, repo) 129 | 130 | self.plugin.parse_pull(pull, summary) 131 | self._analyze_reviews(pull, summary) 132 | self._analyze_comments(pull, summary) 133 | 134 | results.put(summary) 135 | 136 | 137 | def _get_check_status(self, pull, repo): 138 | commits = self.get(pull["commits_url"]) 139 | last = commits[len(commits)-1] 140 | check_suites = self.get('https://api.github.com/repos/%s/%s/commits/%s/check-runs' 141 | % ('abiquo', repo["name"], last["sha"]), 142 | accept='application/vnd.github.antiope-preview+json')['check_runs'] 143 | 144 | checks = {} 145 | for cs in check_suites: 146 | conclusion = cs["conclusion"] if cs["status"] == "completed" else cs["status"] 147 | if conclusion in checks: 148 | check = checks[conclusion] 149 | check['num'] = check['num'] + 1 150 | else: 151 | check = {} 152 | check['num'] = 1 153 | check['unicode'] = self.check_unicodes[conclusion] 154 | checks[conclusion] = check 155 | 156 | return checks 157 | 158 | 159 | def _get_build_status(self, pull): 160 | statuses = self.get(pull['statuses_url']) 161 | return statuses[0]['state'] if statuses else "unknown" 162 | 163 | def _analyze_comments(self, pull, summary): 164 | following = False 165 | comments = self.get(pull["comments_url"]) 166 | comments.extend(self.get(pull["review_comments_url"])) 167 | 168 | for comment in comments: 169 | if comment["user"]["login"] == self.user(): 170 | following = True 171 | self.plugin.parse_comment(comment, summary) 172 | 173 | summary['comments'] = pull["comments"] + pull["review_comments"] 174 | summary['following'] = following 175 | 176 | def _analyze_reviews(self, pull, summary): 177 | reviews = self.get_reviews(pull) 178 | # Index by author, taking only into account approvals or rejections 179 | review_map = defaultdict(list) 180 | for r in filter(lambda r: r['state'] != 'COMMENTED', reviews): 181 | review_map[r['user']['login']].append(r) 182 | for author, author_reviews in review_map.iteritems(): 183 | # Get the last review for each author 184 | review = max(author_reviews, key=lambda r: r['id']) 185 | if review['state'] == 'APPROVED': 186 | summary['likes'] = summary['likes'] + 1 187 | elif review['state'] == 'CHANGES_REQUESTED': 188 | summary['dislikes'] = summary['dislikes'] + 1 189 | 190 | def get_days_old(self, pull): 191 | last_updated = pull['updated_at'] 192 | dt = datetime.datetime.strptime(last_updated, '%Y-%m-%dT%H:%M:%SZ') 193 | today = datetime.datetime.today() 194 | return (today - dt).days 195 | 196 | def get_reviews(self, pull): 197 | return self.get(pull['url'] + '/reviews', 198 | accept='application/vnd.github.black-cat-preview+json') 199 | 200 | def get(self, url, params=None, delay=config.DELAY, 201 | retries=config.MAX_RETRIES, backoff=config.BACKOFF, 202 | accept='application/json'): 203 | if config.DEBUG: 204 | print "GET %s" % url 205 | 206 | headers={'Accept': accept, 'Authorization': 'token ' + self.credentials.token} 207 | while retries > 1: 208 | response = requests.get(url, params=params, headers=headers) 209 | self.__incr_requests() 210 | self.__update_rl(response) 211 | 212 | if response.status_code == requests.codes.ok: 213 | json = response.json() 214 | if "next" in response.links: 215 | json.extend(self.get(response.links['next']["url"])) 216 | return json 217 | elif response.status_code >= 400 and response.status_code < 500: 218 | response.raise_for_status() 219 | elif response.status_code >= 500: 220 | if config.DEBUG: 221 | print "Request failed. Retrying in %s seconds" % delay 222 | time.sleep(delay) 223 | return self.get(url, params, delay * backoff, 224 | retries - 1, backoff) 225 | else: 226 | json = response.json() 227 | if "next" in response.links: 228 | json.extend(self.get(response.links['next']["url"])) 229 | return json 230 | 231 | raise Exception("Request failed after %s retries" % config.MAX_RETRIES) 232 | 233 | def __incr_requests(self): 234 | rlock = threading.RLock() 235 | with rlock: 236 | self.total_requests += 1 237 | 238 | def __update_rl(self, response): 239 | rate_remaining = response.headers['X-RateLimit-Remaining'] 240 | if rate_remaining is not None: 241 | new_rl = int(rate_remaining) 242 | rlock = threading.RLock() 243 | with rlock: 244 | if self.remaining_rl is None or new_rl < self.remaining_rl: 245 | self.remaining_rl = new_rl 246 | 247 | def __incr_threads(self, num_threads): 248 | rlock = threading.RLock() 249 | with rlock: 250 | self.total_threads += num_threads 251 | 252 | def __request_user(self): 253 | url = 'https://api.github.com/user' 254 | r = self.get(url) 255 | self.credentials.user = r['login'] 256 | self.credentials.name = r['name'] 257 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/plugins/__init__.py -------------------------------------------------------------------------------- /plugins/abiquo.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def load(): 5 | return Abiquo() 6 | 7 | 8 | class Abiquo: 9 | def __init__(self): 10 | self.config = { 11 | 'title': 'Abiquo Code Review Dashboard', 12 | 'headers': {'left': 'Cold', 'middle': 'Hot!', 'right': 'Burning'}, 13 | 'template': 'abiquo.html', 14 | } 15 | self.authors = { 16 | 'danielestevez': {'unicode': '128169', 'color': '#7E3817', 'title': 'Lidl Commented'}, 17 | 'apuig': {'unicode': '128092', 'color': '#7E3817', 'title': 'Agorilado Commented'}, 18 | 'enricruiz': {'unicode': '128110', 'color': '#133B78', 'title': 'Thief Commented'}, 19 | 'sergicastro': {'unicode': '9891', 'color': '#C45AEC', 'title': 'Sergi Commented with Love'}, 20 | 'luciaems': {'unicode': '127814', 'color': '#571B7E', 'title': 'Lucia Commented'}, 21 | 'nacx': {'unicode': '128158', 'color': '#FF00DD', 'title': 'Gayer Commented'}, 22 | 'aprete': {'unicode': '127829', 'color': '#CC6600', 'title': 'Rumana Commented'} 23 | } 24 | self.repos = self._abiquo_repos() 25 | 26 | def parse_pull(self, pull, data): 27 | data['obsolete'] = data['old'] >= 2 28 | data['icons'] = [] 29 | 30 | def parse_comment(self, comment, data): 31 | user = comment['user']['login'] 32 | if user in self.authors and not self.authors[user] in data['icons']: 33 | data['icons'].append(self.authors[user]) 34 | 35 | def classify(self, pull): 36 | likes = pull['likes'] 37 | if likes >= 2: 38 | return 'right' 39 | elif likes > 0: 40 | return 'middle' 41 | return 'left' 42 | 43 | def _abiquo_repos(self): 44 | return ["https://api.github.com/repos/abiquo/aim", 45 | "https://api.github.com/repos/abiquo/tarantino", 46 | "https://api.github.com/repos/abiquo/commons-amqp", 47 | "https://api.github.com/repos/abiquo/abiquo-chef-agent", 48 | "https://api.github.com/repos/abiquo/system-properties", 49 | "https://api.github.com/repos/abiquo/appliance-manager", 50 | "https://api.github.com/repos/abiquo/aim-client-java", 51 | "https://api.github.com/repos/abiquo/storage-manager", 52 | "https://api.github.com/repos/abiquo/conversion-manager", 53 | "https://api.github.com/repos/abiquo/monitor-manager", 54 | "https://api.github.com/repos/abiquo/discovery-manager", 55 | "https://api.github.com/repos/abiquo/api", 56 | "https://api.github.com/repos/abiquo/model", 57 | "https://api.github.com/repos/abiquo/storage-plugin-lvm", 58 | "https://api.github.com/repos/abiquo/commons-webapps", 59 | "https://api.github.com/repos/abiquo/task-service", 60 | "https://api.github.com/repos/abiquo/esx-plugin", 61 | "https://api.github.com/repos/abiquo/hyperv-plugin", 62 | "https://api.github.com/repos/abiquo/libvirt-plugin", 63 | "https://api.github.com/repos/abiquo/xenserver-plugin", 64 | "https://api.github.com/repos/abiquo/oraclevm-plugin", 65 | "https://api.github.com/repos/abiquo/hypervisor-plugin-model", 66 | "https://api.github.com/repos/abiquo/commons-redis", 67 | "https://api.github.com/repos/abiquo/nexenta-plugin", 68 | "https://api.github.com/repos/abiquo/netapp-plugin", 69 | "https://api.github.com/repos/abiquo/ec2-plugin", 70 | "https://api.github.com/repos/abiquo/m", 71 | "https://api.github.com/repos/abiquo/commons-test", 72 | "https://api.github.com/repos/abiquo/ui", 73 | "https://api.github.com/repos/abiquo/code-review-dashboard", 74 | "https://api.github.com/repos/abiquo/cloud-provider-proxy", 75 | "https://api.github.com/repos/abiquo/nfs-plugin", 76 | "https://api.github.com/repos/abiquo/jclouds-plugin", 77 | "https://api.github.com/repos/abiquo/platform", 78 | "https://api.github.com/repos/abiquo/kairosdb-java-client", 79 | "https://api.github.com/repos/abiquo/azure-plugin", 80 | "https://api.github.com/repos/abiquo/api-java-client", 81 | "https://api.github.com/repos/abiquo/abiquo-cookbook", 82 | "https://api.github.com/repos/abiquo/ui-tests", 83 | "https://api.github.com/repos/abiquo/docker-plugin", 84 | "https://api.github.com/repos/abiquo/abiquo-reports", 85 | "https://api.github.com/repos/abiquo/collectd-abiquo", 86 | "https://api.github.com/repos/abiquo/collectd-abiquo-cookbook", 87 | "https://api.github.com/repos/abiquo/watchtower", 88 | "https://api.github.com/repos/abiquo/jimmy", 89 | "https://api.github.com/repos/abiquo/nsx-plugin", 90 | "https://api.github.com/repos/abiquo/vcd-plugin", 91 | "https://api.github.com/repos/abiquo/avamar-plugin", 92 | "https://api.github.com/repos/abiquo/veeam-plugin", 93 | "https://api.github.com/repos/abiquo/maven-archetypes", 94 | "https://api.github.com/repos/abiquo/commons-dhcp", 95 | "https://api.github.com/repos/abiquo/oracle-ase", 96 | "https://api.github.com/repos/abiquo/dnsmasq", 97 | "https://api.github.com/repos/abiquo/functional-tests", 98 | "https://api.github.com/repos/abiquo/buchannon", 99 | "https://api.github.com/repos/abiquo/tutorials", 100 | "https://api.github.com/repos/abiquo/networker-plugin"] 101 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | gunicorn==19.9.0 3 | requests==2.20.0 4 | -------------------------------------------------------------------------------- /static/images/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/static/images/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/static/images/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/static/images/apple-touch-icon.png -------------------------------------------------------------------------------- /static/images/dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/static/images/dropdown.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/octocat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiquo/code-review-dashboard/82c20fce271df2bc7a151223f1e8e56e949506f5/static/images/octocat.jpg -------------------------------------------------------------------------------- /static/stylesheets/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V1.2 3 | * Copyright 2011, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 6/20/2012 8 | */ 9 | 10 | 11 | /* Table of Content 12 | ================================================== 13 | #Reset & Basics 14 | #Basic Styles 15 | #Site Styles 16 | #Typography 17 | #Links 18 | #Lists 19 | #Images 20 | #Buttons 21 | #Forms 22 | #Misc */ 23 | 24 | 25 | /* #Reset & Basics (Inspired by E. Meyers) 26 | ================================================== */ 27 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 28 | margin: 0; 29 | padding: 0; 30 | border: 0; 31 | font-size: 100%; 32 | font: inherit; 33 | vertical-align: baseline; } 34 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 35 | display: block; } 36 | body { 37 | line-height: 1; } 38 | ol, ul { 39 | list-style: none; } 40 | blockquote, q { 41 | quotes: none; } 42 | blockquote:before, blockquote:after, 43 | q:before, q:after { 44 | content: ''; 45 | content: none; } 46 | table { 47 | border-collapse: collapse; 48 | border-spacing: 0; } 49 | 50 | 51 | /* #Basic Styles 52 | ================================================== */ 53 | body { 54 | background: #f0f0f0; 55 | font: 14px/21px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 56 | color: #444; 57 | -webkit-font-smoothing: antialiased; /* Fix for webkit rendering */ 58 | -webkit-text-size-adjust: 100%; 59 | } 60 | 61 | 62 | /* #Typography 63 | ================================================== */ 64 | h1, h2, h3, h4, h5, h6 { 65 | color: #181818; 66 | font-family: "Georgia", "Times New Roman", serif; 67 | font-weight: normal; } 68 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; } 69 | h1 { font-size: 46px; line-height: 50px; margin-bottom: 14px;} 70 | h2 { font-size: 35px; line-height: 40px; margin-bottom: 10px; } 71 | h3 { font-size: 28px; line-height: 34px; margin-bottom: 8px; } 72 | h4 { font-size: 21px; line-height: 30px; margin-bottom: 4px; } 73 | h5 { font-size: 17px; line-height: 24px; } 74 | h6 { font-size: 14px; line-height: 21px; } 75 | .subheader { color: #777; } 76 | 77 | p { margin: 0 0 20px 0; } 78 | p img { margin: 0; } 79 | p.lead { font-size: 21px; line-height: 27px; color: #777; } 80 | 81 | em { font-style: italic; } 82 | strong { font-weight: bold; color: #333; } 83 | small { font-size: 80%; } 84 | 85 | /* Blockquotes */ 86 | blockquote, blockquote p { font-size: 17px; line-height: 24px; color: #777; font-style: italic; } 87 | blockquote { margin: 0 0 20px; padding: 9px 20px 0 19px; border-left: 1px solid #ddd; } 88 | blockquote cite { display: block; font-size: 12px; color: #555; } 89 | blockquote cite:before { content: "\2014 \0020"; } 90 | blockquote cite a, blockquote cite a:visited, blockquote cite a:visited { color: #555; } 91 | 92 | hr { border: solid #ddd; border-width: 1px 0 0; clear: both; margin: 10px 0 30px; height: 0; } 93 | 94 | 95 | /* #Links 96 | ================================================== */ 97 | a, a:visited { color: #333; text-decoration: underline; outline: 0; } 98 | a:hover, a:focus { color: #000; } 99 | p a, p a:visited { line-height: inherit; } 100 | 101 | 102 | /* #Lists 103 | ================================================== */ 104 | ul, ol { margin-bottom: 20px; } 105 | ul { list-style: none outside; } 106 | ol { list-style: decimal; } 107 | ol, ul.square, ul.circle, ul.disc { margin-left: 30px; } 108 | ul.square { list-style: square outside; } 109 | ul.circle { list-style: circle outside; } 110 | ul.disc { list-style: disc outside; } 111 | ul ul, ul ol, 112 | ol ol, ol ul { margin: 4px 0 5px 30px; font-size: 90%; } 113 | ul ul li, ul ol li, 114 | ol ol li, ol ul li { margin-bottom: 6px; } 115 | li { line-height: 18px; margin-bottom: 12px; } 116 | ul.large li { line-height: 21px; } 117 | li p { line-height: 21px; } 118 | 119 | /* #Images 120 | ================================================== */ 121 | 122 | img.scale-with-grid { 123 | max-width: 100%; 124 | height: auto; } 125 | 126 | 127 | /* #Buttons 128 | ================================================== */ 129 | 130 | .button, 131 | button, 132 | input[type="submit"], 133 | input[type="reset"], 134 | input[type="button"] { 135 | background: #eee; /* Old browsers */ 136 | background: #eee -moz-linear-gradient(top, rgba(255,255,255,.2) 0%, rgba(0,0,0,.2) 100%); /* FF3.6+ */ 137 | background: #eee -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.2)), color-stop(100%,rgba(0,0,0,.2))); /* Chrome,Safari4+ */ 138 | background: #eee -webkit-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Chrome10+,Safari5.1+ */ 139 | background: #eee -o-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Opera11.10+ */ 140 | background: #eee -ms-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* IE10+ */ 141 | background: #eee linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* W3C */ 142 | border: 1px solid #aaa; 143 | border-top: 1px solid #ccc; 144 | border-left: 1px solid #ccc; 145 | -moz-border-radius: 3px; 146 | -webkit-border-radius: 3px; 147 | border-radius: 3px; 148 | color: #444; 149 | display: inline-block; 150 | font-size: 11px; 151 | font-weight: bold; 152 | text-decoration: none; 153 | text-shadow: 0 1px rgba(255, 255, 255, .75); 154 | cursor: pointer; 155 | margin-bottom: 20px; 156 | line-height: normal; 157 | padding: 8px 10px; 158 | font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; } 159 | 160 | .button:hover, 161 | button:hover, 162 | input[type="submit"]:hover, 163 | input[type="reset"]:hover, 164 | input[type="button"]:hover { 165 | color: #222; 166 | background: #ddd; /* Old browsers */ 167 | background: #ddd -moz-linear-gradient(top, rgba(255,255,255,.3) 0%, rgba(0,0,0,.3) 100%); /* FF3.6+ */ 168 | background: #ddd -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.3)), color-stop(100%,rgba(0,0,0,.3))); /* Chrome,Safari4+ */ 169 | background: #ddd -webkit-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Chrome10+,Safari5.1+ */ 170 | background: #ddd -o-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Opera11.10+ */ 171 | background: #ddd -ms-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* IE10+ */ 172 | background: #ddd linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* W3C */ 173 | border: 1px solid #888; 174 | border-top: 1px solid #aaa; 175 | border-left: 1px solid #aaa; } 176 | 177 | .button:active, 178 | button:active, 179 | input[type="submit"]:active, 180 | input[type="reset"]:active, 181 | input[type="button"]:active { 182 | border: 1px solid #666; 183 | background: #ccc; /* Old browsers */ 184 | background: #ccc -moz-linear-gradient(top, rgba(255,255,255,.35) 0%, rgba(10,10,10,.4) 100%); /* FF3.6+ */ 185 | background: #ccc -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.35)), color-stop(100%,rgba(10,10,10,.4))); /* Chrome,Safari4+ */ 186 | background: #ccc -webkit-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Chrome10+,Safari5.1+ */ 187 | background: #ccc -o-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Opera11.10+ */ 188 | background: #ccc -ms-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* IE10+ */ 189 | background: #ccc linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* W3C */ } 190 | 191 | .button.full-width, 192 | button.full-width, 193 | input[type="submit"].full-width, 194 | input[type="reset"].full-width, 195 | input[type="button"].full-width { 196 | width: 100%; 197 | padding-left: 0 !important; 198 | padding-right: 0 !important; 199 | text-align: center; } 200 | 201 | /* Fix for odd Mozilla border & padding issues */ 202 | button::-moz-focus-inner, 203 | input::-moz-focus-inner { 204 | border: 0; 205 | padding: 0; 206 | } 207 | 208 | 209 | /* #Forms 210 | ================================================== */ 211 | 212 | form { 213 | margin-bottom: 20px; } 214 | fieldset { 215 | margin-bottom: 20px; } 216 | input[type="text"], 217 | input[type="password"], 218 | input[type="email"], 219 | textarea, 220 | select { 221 | border: 1px solid #ccc; 222 | padding: 6px 4px; 223 | outline: none; 224 | -moz-border-radius: 2px; 225 | -webkit-border-radius: 2px; 226 | border-radius: 2px; 227 | font: 13px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 228 | color: #777; 229 | margin: 0; 230 | width: 210px; 231 | max-width: 100%; 232 | display: block; 233 | margin-bottom: 20px; 234 | background: #fff; } 235 | select { 236 | padding: 0; } 237 | input[type="text"]:focus, 238 | input[type="password"]:focus, 239 | input[type="email"]:focus, 240 | textarea:focus { 241 | border: 1px solid #aaa; 242 | color: #444; 243 | -moz-box-shadow: 0 0 3px rgba(0,0,0,.2); 244 | -webkit-box-shadow: 0 0 3px rgba(0,0,0,.2); 245 | box-shadow: 0 0 3px rgba(0,0,0,.2); } 246 | textarea { 247 | min-height: 60px; } 248 | label, 249 | legend { 250 | display: block; 251 | font-weight: bold; 252 | font-size: 13px; } 253 | select { 254 | width: 220px; } 255 | input[type="checkbox"] { 256 | display: inline; } 257 | label span, 258 | legend span { 259 | font-weight: normal; 260 | font-size: 13px; 261 | color: #444; } 262 | 263 | /* #Misc 264 | ================================================== */ 265 | .remove-bottom { margin-bottom: 0 !important; } 266 | .half-bottom { margin-bottom: 10px !important; } 267 | .add-bottom { margin-bottom: 20px !important; } 268 | 269 | 270 | -------------------------------------------------------------------------------- /static/stylesheets/basic.css: -------------------------------------------------------------------------------- 1 | .footer-left { 2 | text-align: left; 3 | color: gray; 4 | margin-bottom: 5px; 5 | } 6 | 7 | .footer-right { 8 | text-align: right; 9 | color: gray; 10 | margin-bottom: 5px; 11 | } 12 | 13 | .footer-center { 14 | text-align: center; 15 | color: gray; 16 | margin-bottom: 5px; 17 | } 18 | 19 | .me { 20 | color: #4863A0; 21 | font-weight: bold; 22 | } 23 | 24 | .count { 25 | color: gray; 26 | } 27 | 28 | .obsolete { 29 | color: red; 30 | } 31 | 32 | .dislike { 33 | color: orange; 34 | } 35 | 36 | .build-unknown { 37 | color: gray; 38 | } 39 | 40 | .build-pending { 41 | color: orange; 42 | } 43 | 44 | .build-success { 45 | color: green; 46 | } 47 | 48 | .build-failure { 49 | color: red; 50 | } 51 | 52 | .check { 53 | color: white; 54 | border-radius: 12px; 55 | padding: 1px 4px; 56 | font-weight: bolder; 57 | } 58 | .check-queued { 59 | background: orange 60 | } 61 | 62 | .check-in_progress { 63 | background: orange 64 | } 65 | 66 | .check-action_required { 67 | background: red 68 | } 69 | 70 | .check-canceled { 71 | background: red 72 | } 73 | 74 | .check-timed_out { 75 | background: red 76 | } 77 | 78 | .check-failed { 79 | background: red 80 | } 81 | 82 | .check-failure { 83 | background: red 84 | } 85 | 86 | .check-success { 87 | background: green 88 | } 89 | 90 | .participated { 91 | color: #4863A0; 92 | } 93 | 94 | #filters { 95 | float: top; 96 | display: none; 97 | width: 100%; 98 | } 99 | 100 | #filters-toggle { 101 | text-align: center; 102 | } 103 | 104 | #dropdown { 105 | cursor: pointer; 106 | } 107 | 108 | .right { 109 | text-align: right; 110 | } 111 | 112 | .signin { 113 | text-align: center; 114 | } 115 | 116 | #octocat { 117 | width: 266px; 118 | height: 221px; 119 | } 120 | 121 | .card { 122 | /* Add shadows to create the "card" effect */ 123 | box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); 124 | transition: 0.3s; 125 | padding: 3px 16px; 126 | border-radius: 7px; 127 | margin: 20px 0px; 128 | background: #fff; 129 | } 130 | 131 | /* On mouse-over, add a deeper shadow */ 132 | .card:hover { 133 | box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); 134 | } 135 | 136 | /* Add some padding inside the card container */ 137 | .container { 138 | } 139 | -------------------------------------------------------------------------------- /static/stylesheets/layout.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V1.2 3 | * Copyright 2011, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 6/20/2012 8 | */ 9 | 10 | /* Table of Content 11 | ================================================== 12 | #Site Styles 13 | #Page Styles 14 | #Media Queries 15 | #Font-Face */ 16 | 17 | /* #Site Styles 18 | ================================================== */ 19 | 20 | /* #Page Styles 21 | ================================================== */ 22 | 23 | /* #Media Queries 24 | ================================================== */ 25 | 26 | /* Smaller than standard 960 (devices and browsers) */ 27 | @media only screen and (max-width: 959px) {} 28 | 29 | /* Tablet Portrait size to standard 960 (devices and browsers) */ 30 | @media only screen and (min-width: 768px) and (max-width: 959px) {} 31 | 32 | /* All Mobile Sizes (devices and browser) */ 33 | @media only screen and (max-width: 767px) {} 34 | 35 | /* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ 36 | @media only screen and (min-width: 480px) and (max-width: 767px) {} 37 | 38 | /* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ 39 | @media only screen and (max-width: 479px) {} 40 | 41 | 42 | /* #Font-Face 43 | ================================================== */ 44 | /* This is the proper syntax for an @font-face file 45 | Just create a "fonts" folder at the root, 46 | copy your FontName into code below and remove 47 | comment brackets */ 48 | 49 | /* @font-face { 50 | font-family: 'FontName'; 51 | src: url('../fonts/FontName.eot'); 52 | src: url('../fonts/FontName.eot?iefix') format('eot'), 53 | url('../fonts/FontName.woff') format('woff'), 54 | url('../fonts/FontName.ttf') format('truetype'), 55 | url('../fonts/FontName.svg#webfontZam02nTh') format('svg'); 56 | font-weight: normal; 57 | font-style: normal; } 58 | */ -------------------------------------------------------------------------------- /static/stylesheets/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V1.2 3 | * Copyright 2011, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 6/20/2012 8 | */ 9 | 10 | 11 | /* Table of Contents 12 | ================================================== 13 | #Base 960 Grid 14 | #Tablet (Portrait) 15 | #Mobile (Portrait) 16 | #Mobile (Landscape) 17 | #Clearing */ 18 | 19 | 20 | 21 | /* #Base 960 Grid 22 | ================================================== */ 23 | 24 | .container { position: relative; width: 960px; margin: 0 auto; padding: 0; } 25 | .container .column, 26 | .container .columns { float: left; display: inline; margin-left: 10px; margin-right: 10px; } 27 | .row { margin-bottom: 20px; } 28 | 29 | /* Nested Column Classes */ 30 | .column.alpha, .columns.alpha { margin-left: 0; } 31 | .column.omega, .columns.omega { margin-right: 0; } 32 | 33 | /* Base Grid */ 34 | .container .one.column, 35 | .container .one.columns { width: 40px; } 36 | .container .two.columns { width: 100px; } 37 | .container .three.columns { width: 160px; } 38 | .container .four.columns { width: 220px; } 39 | .container .five.columns { width: 280px; } 40 | .container .six.columns { width: 340px; } 41 | .container .seven.columns { width: 400px; } 42 | .container .eight.columns { width: 460px; } 43 | .container .nine.columns { width: 520px; } 44 | .container .ten.columns { width: 580px; } 45 | .container .eleven.columns { width: 640px; } 46 | .container .twelve.columns { width: 700px; } 47 | .container .thirteen.columns { width: 760px; } 48 | .container .fourteen.columns { width: 820px; } 49 | .container .fifteen.columns { width: 880px; } 50 | .container .sixteen.columns { width: 940px; } 51 | 52 | .container .one-third.column { width: 300px; } 53 | .container .two-thirds.column { width: 620px; } 54 | 55 | /* Offsets */ 56 | .container .offset-by-one { padding-left: 60px; } 57 | .container .offset-by-two { padding-left: 120px; } 58 | .container .offset-by-three { padding-left: 180px; } 59 | .container .offset-by-four { padding-left: 240px; } 60 | .container .offset-by-five { padding-left: 300px; } 61 | .container .offset-by-six { padding-left: 360px; } 62 | .container .offset-by-seven { padding-left: 420px; } 63 | .container .offset-by-eight { padding-left: 480px; } 64 | .container .offset-by-nine { padding-left: 540px; } 65 | .container .offset-by-ten { padding-left: 600px; } 66 | .container .offset-by-eleven { padding-left: 660px; } 67 | .container .offset-by-twelve { padding-left: 720px; } 68 | .container .offset-by-thirteen { padding-left: 780px; } 69 | .container .offset-by-fourteen { padding-left: 840px; } 70 | .container .offset-by-fifteen { padding-left: 900px; } 71 | 72 | 73 | 74 | /* #Tablet (Portrait) 75 | ================================================== */ 76 | 77 | /* Note: Design for a width of 768px */ 78 | 79 | @media only screen and (min-width: 768px) and (max-width: 959px) { 80 | .container { width: 768px; } 81 | .container .column, 82 | .container .columns { margin-left: 10px; margin-right: 10px; } 83 | .column.alpha, .columns.alpha { margin-left: 0; margin-right: 10px; } 84 | .column.omega, .columns.omega { margin-right: 0; margin-left: 10px; } 85 | .alpha.omega { margin-left: 0; margin-right: 0; } 86 | 87 | .container .one.column, 88 | .container .one.columns { width: 28px; } 89 | .container .two.columns { width: 76px; } 90 | .container .three.columns { width: 124px; } 91 | .container .four.columns { width: 172px; } 92 | .container .five.columns { width: 220px; } 93 | .container .six.columns { width: 268px; } 94 | .container .seven.columns { width: 316px; } 95 | .container .eight.columns { width: 364px; } 96 | .container .nine.columns { width: 412px; } 97 | .container .ten.columns { width: 460px; } 98 | .container .eleven.columns { width: 508px; } 99 | .container .twelve.columns { width: 556px; } 100 | .container .thirteen.columns { width: 604px; } 101 | .container .fourteen.columns { width: 652px; } 102 | .container .fifteen.columns { width: 700px; } 103 | .container .sixteen.columns { width: 748px; } 104 | 105 | .container .one-third.column { width: 236px; } 106 | .container .two-thirds.column { width: 492px; } 107 | 108 | /* Offsets */ 109 | .container .offset-by-one { padding-left: 48px; } 110 | .container .offset-by-two { padding-left: 96px; } 111 | .container .offset-by-three { padding-left: 144px; } 112 | .container .offset-by-four { padding-left: 192px; } 113 | .container .offset-by-five { padding-left: 240px; } 114 | .container .offset-by-six { padding-left: 288px; } 115 | .container .offset-by-seven { padding-left: 336px; } 116 | .container .offset-by-eight { padding-left: 384px; } 117 | .container .offset-by-nine { padding-left: 432px; } 118 | .container .offset-by-ten { padding-left: 480px; } 119 | .container .offset-by-eleven { padding-left: 528px; } 120 | .container .offset-by-twelve { padding-left: 576px; } 121 | .container .offset-by-thirteen { padding-left: 624px; } 122 | .container .offset-by-fourteen { padding-left: 672px; } 123 | .container .offset-by-fifteen { padding-left: 720px; } 124 | } 125 | 126 | 127 | /* #Mobile (Portrait) 128 | ================================================== */ 129 | 130 | /* Note: Design for a width of 320px */ 131 | 132 | @media only screen and (max-width: 767px) { 133 | .container { width: 300px; } 134 | .container .columns, 135 | .container .column { margin: 0; } 136 | 137 | .container .one.column, 138 | .container .one.columns, 139 | .container .two.columns, 140 | .container .three.columns, 141 | .container .four.columns, 142 | .container .five.columns, 143 | .container .six.columns, 144 | .container .seven.columns, 145 | .container .eight.columns, 146 | .container .nine.columns, 147 | .container .ten.columns, 148 | .container .eleven.columns, 149 | .container .twelve.columns, 150 | .container .thirteen.columns, 151 | .container .fourteen.columns, 152 | .container .fifteen.columns, 153 | .container .sixteen.columns, 154 | .container .one-third.column, 155 | .container .two-thirds.column { width: 300px; } 156 | 157 | /* Offsets */ 158 | .container .offset-by-one, 159 | .container .offset-by-two, 160 | .container .offset-by-three, 161 | .container .offset-by-four, 162 | .container .offset-by-five, 163 | .container .offset-by-six, 164 | .container .offset-by-seven, 165 | .container .offset-by-eight, 166 | .container .offset-by-nine, 167 | .container .offset-by-ten, 168 | .container .offset-by-eleven, 169 | .container .offset-by-twelve, 170 | .container .offset-by-thirteen, 171 | .container .offset-by-fourteen, 172 | .container .offset-by-fifteen { padding-left: 0; } 173 | 174 | } 175 | 176 | 177 | /* #Mobile (Landscape) 178 | ================================================== */ 179 | 180 | /* Note: Design for a width of 480px */ 181 | 182 | @media only screen and (min-width: 480px) and (max-width: 767px) { 183 | .container { width: 420px; } 184 | .container .columns, 185 | .container .column { margin: 0; } 186 | 187 | .container .one.column, 188 | .container .one.columns, 189 | .container .two.columns, 190 | .container .three.columns, 191 | .container .four.columns, 192 | .container .five.columns, 193 | .container .six.columns, 194 | .container .seven.columns, 195 | .container .eight.columns, 196 | .container .nine.columns, 197 | .container .ten.columns, 198 | .container .eleven.columns, 199 | .container .twelve.columns, 200 | .container .thirteen.columns, 201 | .container .fourteen.columns, 202 | .container .fifteen.columns, 203 | .container .sixteen.columns, 204 | .container .one-third.column, 205 | .container .two-thirds.column { width: 420px; } 206 | } 207 | 208 | 209 | /* #Clearing 210 | ================================================== */ 211 | 212 | /* Self Clearing Goodness */ 213 | .container:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; } 214 | 215 | /* Use clearfix class on parent to clear nested columns, 216 | or wrap each row of columns in a
*/ 217 | .clearfix:before, 218 | .clearfix:after, 219 | .row:before, 220 | .row:after { 221 | content: '\0020'; 222 | display: block; 223 | overflow: hidden; 224 | visibility: hidden; 225 | width: 0; 226 | height: 0; } 227 | .row:after, 228 | .clearfix:after { 229 | clear: both; } 230 | .row, 231 | .clearfix { 232 | zoom: 1; } 233 | 234 | /* You can also use a
to clear columns */ 235 | .clear { 236 | clear: both; 237 | display: block; 238 | overflow: hidden; 239 | visibility: hidden; 240 | width: 0; 241 | height: 0; 242 | } 243 | -------------------------------------------------------------------------------- /templates/abiquo.html: -------------------------------------------------------------------------------- 1 | {% macro render(pull, user) -%} 2 | {{ pull['repo_name'] }} 3 | [{{ pull['target_branch'] }}] {{ pull['name'] }} ({{ pull['author'] }})
4 | {% for icon in pull['icons'] %} 5 | &#{{ icon['unicode'] }}; 6 | {% endfor %} 7 | {{ pull['likes'] }} likes / 8 | {% if pull['dislikes'] > 0 %} 9 | {{ pull['dislikes'] }} dislikes / 10 | {% endif %} 11 | {% if pull['following'] %}{% endif %} 12 | {{ pull['comments'] }} comments 13 | {% if pull['following'] %}{% endif %} / 14 | {% if pull['obsolete'] %}{% endif %} 15 | {{ pull['old'] }} days old 16 | {% if pull['obsolete'] %}{% endif %}
17 | {% if pull['checks'] %} 18 | Check status: 19 | {% for checkstatus, check in pull['checks'].iteritems() %} 20 | &#{{ check['unicode'] }};: {{ check['num'] }} 21 | {% endfor %} 22 | 23 | {% endif %} 24 | 25 | {%- endmacro %} 26 | -------------------------------------------------------------------------------- /templates/columns.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import template as renderer %} 4 | 5 | {% block header %} 6 | {% include 'filters.html' %} 7 | {% endblock %} 8 | 9 | {% block columns %} 10 | 16 | 22 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/filters.html: -------------------------------------------------------------------------------- 1 | 2 | 76 |
77 | 87 | 97 | 107 |
108 | Show only my pulls 109 |
110 |
111 | Remove all filters 112 |
113 |
114 |
115 | 116 |
117 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %} 10 | {{ title }} 11 | {% endblock %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Fork me on GitHub 38 | 39 | 40 |
41 | {% block header %} 42 |
43 |   44 |
45 | {% endblock %} 46 | 47 | {% block columns %} 48 | {% endblock %} 49 | 50 |
51 |
52 |
53 | 54 | {% block footer %} 55 | 59 | 64 | 67 | {% endblock %} 68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /templates/signin.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block header %} 4 | 8 | {% endblock %} 9 | 10 | {% block columns %} 11 | 22 | {% endblock %} 23 | 24 | {% block footer %} 25 | {% endblock %} 26 | --------------------------------------------------------------------------------