├── tools ├── __init__.py ├── rename_checklist_type.py └── rename_tag.py ├── pushmanager ├── __init__.py ├── core │ ├── __init__.py │ ├── settings.py │ ├── auth.py │ ├── application.py │ ├── rb.py │ ├── pid.py │ ├── requesthandler.py │ └── mail.py ├── servlets │ ├── __init__.py │ ├── conflictcheck.py │ ├── pushitems.py │ ├── pushbyrequest.py │ ├── userlist.py │ ├── request.py │ ├── editpush.py │ ├── pushes.py │ ├── summaryforbranch.py │ ├── requests.py │ ├── undelayrequest.py │ ├── smartdest.py │ ├── push.py │ ├── pingme.py │ ├── msg.py │ ├── discardpush.py │ ├── verifyrequest.py │ ├── discardrequest.py │ ├── delayrequest.py │ ├── commentrequest.py │ ├── addrequest.py │ ├── newpush.py │ ├── blesspush.py │ ├── removerequest.py │ ├── pickmerequest.py │ └── livepush.py ├── static │ ├── favicon.ico │ ├── img │ │ ├── favicon.gif │ │ ├── hamster.png │ │ ├── ajax-loader.gif │ │ ├── button_expand.gif │ │ └── button_hide.gif │ ├── css │ │ ├── flick │ │ │ └── images │ │ │ │ ├── ui-icons_0073ea_256x240.png │ │ │ │ ├── ui-icons_454545_256x240.png │ │ │ │ ├── ui-icons_666666_256x240.png │ │ │ │ ├── ui-icons_ff0084_256x240.png │ │ │ │ ├── ui-icons_ffffff_256x240.png │ │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ │ ├── ui-bg_flat_0_eeeeee_40x100.png │ │ │ │ ├── ui-bg_flat_55_ffffff_40x100.png │ │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ │ ├── ui-bg_highlight-soft_100_f6f6f6_1x100.png │ │ │ │ ├── ui-bg_highlight-soft_25_0073ea_1x100.png │ │ │ │ └── ui-bg_highlight-soft_50_dddddd_1x100.png │ │ └── modules │ │ │ ├── newrequest.css │ │ │ └── request.css │ └── js │ │ └── modules │ │ └── newrequest.js ├── testing │ ├── __init__.py │ ├── mocksettings.py │ └── testdb.sql ├── tests │ ├── __init__.py │ ├── test_core_auth.py │ ├── test_core_requesthandler.py │ ├── test_core_mail.py │ ├── test_servlet_login.py │ ├── test_template_pushes.py │ ├── test_ui_methods.py │ ├── test_servlet_pushitems.py │ ├── test_bookmarklet.py │ ├── test_servlet_pushes.py │ ├── test_template_newrequest.py │ ├── test_servlet_summaryforbranch.py │ ├── test_servlet_livepush.py │ ├── test_core_pid.py │ ├── test_rename_tag.py │ ├── test_servlet_delayrequest.py │ ├── test_servlet_discardrequest.py │ ├── test_servlet_pickmerequests.py │ ├── test_servlet_push.py │ ├── test_servlet_deploypush.py │ ├── test_servlet_blesspush.py │ ├── test_servlet_msg.py │ ├── test_core_db.py │ ├── test_servlet_api.py │ └── test_rename_checklist_type.py ├── __about__.py ├── templates │ ├── home.html │ ├── request.html │ ├── userlist.html │ ├── confirm-conflict-check.html │ ├── checklist │ │ └── category.html │ ├── login.html │ ├── requests.html │ ├── new-push.html │ ├── edit-push.html │ ├── push.html │ ├── modules │ │ ├── request-info.html │ │ ├── request-buttons.html │ │ ├── request.html │ │ └── newrequest.html │ ├── push-button-bar.html │ ├── check_sites_bookmarklet.js │ ├── push-info.html │ ├── pushitems.html │ ├── checklist.html │ ├── create_request_bookmarklet.js │ ├── push-dialogs.html │ ├── base.html │ └── push-status.html ├── ui_methods.py ├── pushmanager_api.py ├── handlers.py └── ui_modules.py ├── requirements.txt ├── .travis.yml ├── MANIFEST.in ├── requirements-dev.txt ├── CONTRIBUTORS ├── README.rst ├── Makefile ├── pushplans ├── add_stageenv.sql ├── add_conflicts.sql ├── add_watchers.sql └── rename_push_to_pushplan.sh ├── .coveragerc ├── .gitignore ├── tox.ini ├── setup.py ├── CHANGELOG ├── UPDATING └── config.yaml.example /tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/servlets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/static/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/testing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pushmanager/__about__.py: -------------------------------------------------------------------------------- 1 | __version_info__ = (0, 3, 6) 2 | __version__ = '%d.%d.%d' % __version_info__ 3 | -------------------------------------------------------------------------------- /pushmanager/static/img/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/img/favicon.gif -------------------------------------------------------------------------------- /pushmanager/static/img/hamster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/img/hamster.png -------------------------------------------------------------------------------- /pushmanager/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /pushmanager/static/img/button_expand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/img/button_expand.gif -------------------------------------------------------------------------------- /pushmanager/static/img/button_hide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/img/button_hide.gif -------------------------------------------------------------------------------- /pushmanager/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}Home{% end %} 4 | 5 | {% block content %} 6 | {% end %} 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml==2.2.4 2 | mysql-python==1.2.5 3 | python-daemon==1.5.2 4 | python-ldap==2.4.13 5 | tornado==2.4.1 6 | xmpppy==0.5.0rc1 7 | -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_0073ea_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-icons_0073ea_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_666666_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-icons_666666_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_ff0084_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-icons_ff0084_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_0_eeeeee_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_flat_0_eeeeee_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_55_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_flat_55_ffffff_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_highlight-soft_25_0073ea_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_highlight-soft_25_0073ea_1x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_highlight-soft_50_dddddd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/pushmanager/master/pushmanager/static/css/flick/images/ui-bg_highlight-soft_50_dddddd_1x100.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | env: # These should match the tox env list 3 | - TOXENV=py 4 | 5 | before_install: 6 | - pip install --use-mirrors mysql-python==1.2.5 python-ldap==2.4.13 7 | 8 | install: pip install tox --use-mirrors 9 | script: tox 10 | -------------------------------------------------------------------------------- /pushmanager/testing/mocksettings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import copy 4 | import sys 5 | 6 | 7 | sys.path.append(".") 8 | sys.path.append("..") 9 | 10 | from pushmanager.core.settings import Settings 11 | 12 | 13 | MockedSettings = copy.deepcopy(Settings) 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include config.yaml.example 2 | include pushmanager/testing/*sql 3 | include README.rst 4 | include requirements-dev.txt 5 | include requirements.txt 6 | include setup.py 7 | include tox.ini 8 | recursive-include pushmanager/static * 9 | recursive-include pushmanager/templates * 10 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # If there are tools that a developer needs to work on your project. 2 | # Unit testing tools, mocking frameworks, debuggers, proxies, ... 3 | # These will be used for tox and other testing environments. 4 | -r requirements.txt 5 | testify==0.5.3 6 | flake8==2.1.0 7 | coverage==3.7 8 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Amber Yust 2 | Baris Metin 3 | Evan Krall 4 | James Brown 5 | James Duncan 6 | Jon Madden 7 | Josh Snyder 8 | Sam Kimbrel 9 | Tyler Roscoe -------------------------------------------------------------------------------- /pushmanager/ui_methods.py: -------------------------------------------------------------------------------- 1 | def authorized_to_manage_request(_, request, current_user, pushmaster=False): 2 | if pushmaster or \ 3 | request['user'] == current_user or \ 4 | (request['watchers'] and current_user in request['watchers'].split(',')): 5 | return True 6 | return False 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pushmanager 2 | =========== 3 | 4 | Pushmanager is a tornado web application we use to manage deployments 5 | at Yelp. It helps pushmasters to conduct the deployment by bringing 6 | together push requests from engineers and information gathered from 7 | reviews, test builds and issue tracking system. 8 | 9 | TODO: 10 | README update 11 | Changelog 12 | -------------------------------------------------------------------------------- /pushmanager/templates/request.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}{{ escape(page_title) }}{% end %} 4 | 5 | {% block content %} 6 |
    7 |
  • {{ modules.NewRequestDialog() }}
  • 8 |
9 | {{ modules.Request(req, expand=True, edit_buttons=(req['state'] not in ('live', 'discarded')), show_state_inline=True, show_ago=True) }} 10 | {% end %} 11 | -------------------------------------------------------------------------------- /pushmanager/tests/test_core_auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | 4 | import mock 5 | import testify as T 6 | from pushmanager.core import auth 7 | 8 | 9 | class TestAuthenticaton(T.TestCase): 10 | 11 | def test_authenticate(self): 12 | with mock.patch.object(logging, "exception"): 13 | T.assert_equal(auth.authenticate("fake_user", "fake_password"), False) 14 | -------------------------------------------------------------------------------- /pushmanager/templates/userlist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}Users{% end %} 4 | 5 | {% block content %} 6 |
    7 | {% for (letter, users) in sorted(users_by_alpha.iteritems()) %} 8 |
  • {{ escape(letter.upper()) }}
  • 13 | {% end %} 14 |
15 | {% end %} 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test flakes 2 | @echo "Default target is to test. You need to specify other targets explicitly." 3 | 4 | .PHONY: flakes 5 | flakes: 6 | tox 7 | 8 | .PHONY: test 9 | test: 10 | tox 11 | 12 | .PHONY: coverage 13 | coverage: 14 | tox -e cover 15 | coverage html 16 | coverage xml 17 | 18 | .PHONY: tests 19 | tests: test ; 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -rf .coverage 24 | rm -rf .tox 25 | find . -name '*.pyc' -delete 26 | find . -name '__pycache__' -delete 27 | -------------------------------------------------------------------------------- /pushplans/add_stageenv.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Add stagenv column to pushes 3 | */ 4 | 5 | # MySQL Syntax 6 | ALTER TABLE `push_pushes` 7 | ADD COLUMN `stageenv` text default NULL AFTER `extra_pings`; 8 | 9 | /* ROLLBACK COMMANDS 10 | 11 | ALTER TABLE `push_pushes`` 12 | DROP COLUMN `stageenv`; 13 | 14 | */ 15 | 16 | # Sqlite3 Syntax 17 | # WARNING: BACKUP DATABASE FIRST! 18 | # sqlite3 has no rollback equivalent for add column 19 | /* 20 | ALTER TABLE 'push_pushes' 21 | ADD COLUMN 'stageenv' VARCHAR; 22 | */ 23 | -------------------------------------------------------------------------------- /pushplans/add_conflicts.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Add conflicts column to push_requests 3 | */ 4 | 5 | # MySQL Syntax 6 | ALTER TABLE `push_requests` 7 | ADD COLUMN `conflicts` text default NULL AFTER `tags`; 8 | 9 | /* ROLLBACK COMMANDS 10 | 11 | ALTER TABLE `push_requests` 12 | DROP COLUMN `conflicts`; 13 | 14 | */ 15 | 16 | # Sqlite3 Syntax 17 | # WARNING: BACKUP DATABASE FIRST! 18 | # sqlite3 has no rollback equivalent for add column 19 | /* 20 | ALTER TABLE 'push_requests' 21 | ADD COLUMN 'conflicts' VARCHAR; 22 | */ 23 | -------------------------------------------------------------------------------- /pushplans/add_watchers.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Add watchers column to push_requests. 3 | */ 4 | 5 | # MySQL Syntax 6 | ALTER TABLE `push_requests` 7 | ADD COLUMN `watchers` text default NULL AFTER `description`; 8 | 9 | /* ROLLBACK COMMANDS 10 | 11 | ALTER TABLE `push_requests` 12 | DROP COLUMN `watchers`; 13 | 14 | */ 15 | 16 | # Sqlite3 Syntax 17 | # WARNING: BACKUP DATABASE FIRST! 18 | # sqlite3 has no rollback equivalent for add column 19 | /* 20 | ALTER TABLE 'push_requests' 21 | ADD COLUMN 'watchers' VARCHAR; 22 | */ 23 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | timid = True 4 | source = 5 | . 6 | omit = 7 | class_under_test.py 8 | *__init__.py 9 | testing/* 10 | tests/* 11 | */tests/* 12 | 13 | [report] 14 | exclude_lines = 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if __name__ == .__main__.: 21 | ignore_errors = True 22 | 23 | [html] 24 | directory = coverage-html 25 | 26 | -------------------------------------------------------------------------------- /pushmanager/templates/confirm-conflict-check.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /pushmanager/tests/test_core_requesthandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import mock 4 | import testify as T 5 | from pushmanager.core.requesthandler import RequestHandler 6 | from pushmanager.core.settings import Settings 7 | from pushmanager.testing.mocksettings import MockedSettings 8 | 9 | 10 | class RequestHandlerTest(T.TestCase): 11 | 12 | def test_get_api_page(self): 13 | MockedSettings['api_app'] = {'port': 8043, 'servername': 'push.test.com'} 14 | with mock.patch.dict(Settings, MockedSettings): 15 | T.assert_equal( 16 | RequestHandler.get_api_page("pushes"), 17 | "http://push.test.com:8043/api/pushes" 18 | ) 19 | -------------------------------------------------------------------------------- /pushmanager/servlets/conflictcheck.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | from pushmanager.core.git import GitQueue 3 | from pushmanager.core.git import GitTaskAction 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class ConflictCheckServlet(RequestHandler): 8 | 9 | def _arg(self, key): 10 | return pushmanager.core.util.get_str_arg(self.request, key, '') 11 | 12 | def post(self): 13 | if not self.current_user: 14 | return self.send_error(403) 15 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 16 | GitQueue.enqueue_request(GitTaskAction.TEST_ALL_PICKMES, self.pushid) 17 | self.redirect("/push?id=%d" % self.pushid) 18 | -------------------------------------------------------------------------------- /pushmanager/servlets/pushitems.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class PushItemsServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.gen.engine 11 | def get(self): 12 | pushid = pushmanager.core.util.get_int_arg(self.request, 'push', None) 13 | 14 | response = yield tornado.gen.Task( 15 | self.async_api_call, 16 | "pushitems", 17 | {"push_id": pushid} 18 | ) 19 | 20 | results = self.get_api_results(response) 21 | self.render("pushitems.html", requests=results) 22 | -------------------------------------------------------------------------------- /pushmanager/tests/test_core_mail.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pushmanager.core.mail 3 | import testify as T 4 | 5 | 6 | class MailQueueTest(T.TestCase): 7 | 8 | def test_send_mail(self): 9 | with mock.patch.object(pushmanager.core.mail.MailQueue, "smtp") as mocked_smtp: 10 | with mock.patch.object(pushmanager.core.mail.MailQueue, "message_queue"): 11 | recipient = "test@test.com" 12 | message = "test message" 13 | subject = "test subject" 14 | from_email = "fromtest@test.com" 15 | 16 | pushmanager.core.mail.MailQueue._send_email(recipient, message, subject, from_email) 17 | 18 | T.assert_equal(mocked_smtp.sendmail.called, True) 19 | -------------------------------------------------------------------------------- /pushmanager/servlets/pushbyrequest.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class PushByRequestServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.gen.engine 11 | def get(self): 12 | requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 13 | response = yield tornado.gen.Task( 14 | self.async_api_call, 15 | "pushbyrequest", 16 | {"id": requestid} 17 | ) 18 | 19 | push = self.get_api_results(response) 20 | if push: 21 | self.redirect('/push?id=%s' % push['id']) 22 | -------------------------------------------------------------------------------- /pushmanager/templates/checklist/category.html: -------------------------------------------------------------------------------- 1 | {% if items_by_target.get(target, []) %} 2 |

{{ title }}

3 |
    4 | {% for item in items_by_target.get(target, []) %} 5 |
  • 7 | 13 |
  • 14 | {% end %} 15 |
16 | {% end %} 17 | -------------------------------------------------------------------------------- /pushmanager/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}Login{% end %} 4 | 5 | {% block content %} 6 | {% if errors %} 7 |

{{ errors }}

8 | {% end %} 9 |
10 | {% if next_url %}{% end %} 11 | 12 | 13 | 14 | 15 | 16 |
17 | {% end %} 18 | 19 | {% block scripts %} 20 | 25 | {% end %} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Sphinx documentation 38 | docs/_build/ 39 | 40 | # Editors 41 | [._]*.s[a-w][a-z] 42 | [._]s[a-w][a-z] 43 | *.un~ 44 | Session.vim 45 | .netrwhist 46 | *~ 47 | 48 | # Pushmanager 49 | app.*.log 50 | app.*.pid 51 | /config.yaml 52 | -------------------------------------------------------------------------------- /pushmanager/templates/requests.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}{{ escape(page_title) }}{% end %} 4 | 5 | {% block content %} 6 |
    7 |
  • {{ modules.NewRequestDialog() }}
  • 8 | {% if show_count %} 9 |
  • {{ len(list(requests)) }} requests
  • 10 | {% end %} 11 |
12 |
    13 | {% for request in requests %} 14 |
  • 15 |
     
    16 | {{ modules.Request(request, edit_buttons=(request['state'] not in ('live', 'discarded')), show_state_inline=True, show_ago=True) }} 17 |
  • 18 | {% end %} 19 |
20 | {% end %} 21 | -------------------------------------------------------------------------------- /pushmanager/servlets/userlist.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import tornado.gen 4 | import tornado.web 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class UserListServlet(RequestHandler): 9 | 10 | @tornado.web.asynchronous 11 | @tornado.web.authenticated 12 | @tornado.gen.engine 13 | def get(self): 14 | response = yield tornado.gen.Task( 15 | self.async_api_call, 16 | "userlist", 17 | {} 18 | ) 19 | 20 | users_by_alpha = defaultdict(list) 21 | map( 22 | lambda u: users_by_alpha[u[0]].append(u), 23 | self.get_api_results(response) 24 | ) 25 | 26 | self.render("userlist.html", page_title="Users", users_by_alpha=users_by_alpha) 27 | -------------------------------------------------------------------------------- /pushmanager/tests/test_servlet_login.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import urllib 3 | 4 | import mock 5 | import testify as T 6 | from pushmanager.handlers import LoginHandler 7 | from pushmanager.testing.testservlet import ServletTestMixin 8 | 9 | 10 | class LoginTest(T.TestCase, ServletTestMixin): 11 | 12 | def get_handlers(self): 13 | return [(r'/login', LoginHandler)] 14 | 15 | def test_login_post(self): 16 | request = { 17 | "username": "fake_username", 18 | "password": "fake_password" 19 | } 20 | with mock.patch.object(logging, "exception"): 21 | response = self.fetch( 22 | "/login", 23 | method="POST", 24 | body=urllib.urlencode(request) 25 | ) 26 | T.assert_in("Invalid username or password specified.", response.body) 27 | -------------------------------------------------------------------------------- /pushmanager/servlets/request.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class RequestServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.web.authenticated 11 | @tornado.gen.engine 12 | def get(self): 13 | request_id = pushmanager.core.util.get_int_arg(self.request, 'id') 14 | if not request_id: 15 | self.send_error(404) 16 | 17 | response = yield tornado.gen.Task( 18 | self.async_api_call, 19 | "request", 20 | {'id': request_id} 21 | ) 22 | 23 | req = self.get_api_results(response) 24 | if not req: 25 | self.send_error() 26 | 27 | self.render("request.html", page_title="Request #%d" % request_id, req=req) 28 | -------------------------------------------------------------------------------- /pushmanager/static/css/modules/newrequest.css: -------------------------------------------------------------------------------- 1 | #request-info-form { 2 | position: absolute; 3 | top: 20px; 4 | right: 20px; 5 | bottom: 20px; 6 | left: 20px; 7 | } 8 | 9 | #request-info-form > fieldset { 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | } 16 | 17 | #request-info-form input, 18 | #request-info-form textarea { 19 | display: block; 20 | width: 99%; 21 | margin-bottom: 10px; 22 | } 23 | 24 | #request-info-form textarea { 25 | height: 100px; 26 | font-family: monospace; 27 | } 28 | 29 | #request-info-form #request-form-comments, 30 | #request-info-form #request-form-description { 31 | position: relative; 32 | } 33 | 34 | #request-info-form input[type=submit] { 35 | width: 150px; 36 | float: right; 37 | } 38 | 39 | #request-info-form label { 40 | display: block; 41 | margin-top: 10px; 42 | } 43 | 44 | #request-info-form #request-form-takeover { 45 | float: left; 46 | } 47 | -------------------------------------------------------------------------------- /pushplans/rename_push_to_pushplan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ############################################################ 3 | # Convert plans to pushplans 4 | # 5 | # Run: 6 | # sh pushplans/rename_push_to_pushplan.sh 7 | # from the production pushmanager root such that the 8 | # production config.yaml is present and readable 9 | ############################################################ 10 | SCRIPT=$(readlink -f $0) 11 | SCRIPT_DIR=$(dirname $SCRIPT) 12 | 13 | [ ! -r config.yaml ] && echo "No readable config.yaml present in the current directory" && exit 1 14 | 15 | PYTHONPATH=. python -u "${SCRIPT_DIR}/../tools/rename_tag.py" plans pushplans 16 | PYTHONPATH=. python -u "${SCRIPT_DIR}/../tools/rename_checklist_type.py" plans pushplans 17 | 18 | # Revert steps if needed 19 | #PYTHONPATH=. python -u "${SCRIPT_DIR}/../tools/rename_tag.py" pushplans plans 20 | #PYTHONPATH=. python -u "${SCRIPT_DIR}/../tools/rename_checklist_type.py" pushplans plans 21 | -------------------------------------------------------------------------------- /pushmanager/tests/test_template_pushes.py: -------------------------------------------------------------------------------- 1 | import testify as T 2 | from pushmanager.testing.testservlet import TemplateTestCase 3 | 4 | 5 | class PushesTemplateTest(TemplateTestCase): 6 | 7 | authenticated = True 8 | pushes_page = 'pushes.html' 9 | new_push_page = 'new-push.html' 10 | 11 | def render_pushes_page(self, page_title='Pushes', pushes=[], pushes_per_page=50, last_push=None): 12 | return self.render_etree(self.pushes_page, 13 | page_title=page_title, 14 | pushes=pushes, 15 | rpp=pushes_per_page, 16 | last_push=last_push 17 | ) 18 | 19 | def test_include_new_push(self): 20 | tree = self.render_pushes_page() 21 | 22 | found_form = [] 23 | for form in tree.iter('form'): 24 | if form.attrib['id'] == 'push-info-form': 25 | found_form.append(form) 26 | 27 | T.assert_equal(len(found_form), 1) 28 | 29 | 30 | if __name__ == '__main__': 31 | T.run() 32 | -------------------------------------------------------------------------------- /pushmanager/templates/new-push.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /pushmanager/servlets/editpush.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class EditPushServlet(RequestHandler): 9 | 10 | def _arg(self, key): 11 | return pushmanager.core.util.get_str_arg(self.request, key, '') 12 | 13 | def post(self): 14 | if not self.current_user: 15 | return self.send_error(403) 16 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 17 | query = db.push_pushes.update().where(db.push_pushes.c.id == self.pushid).values(**{ 18 | 'title': self._arg('push-title'), 19 | 'user': self.current_user, 20 | 'branch': self._arg('push-branch'), 21 | 'stageenv': self._arg('push-stageenv'), 22 | 'revision': "0"*40, 23 | 'modified': time.time(), 24 | }) 25 | db.execute_cb( 26 | query, 27 | lambda _, __: self.redirect("/push?id=%d" % self.pushid) 28 | ) 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py 3 | 4 | [testenv] 5 | deps = -rrequirements-dev.txt 6 | setenv = 7 | SERVICE_ENV_CONFIG_PATH = config.yaml.example 8 | 9 | [testenv:py] 10 | deps = {[testenv]deps} 11 | commands = 12 | testify {posargs:pushmanager.tests} 13 | pyflakes pushmanager 14 | pyflakes pushmanager/tests 15 | pyflakes setup.py 16 | # flake8 pushmanager 17 | # flake8 pushmanager/tests 18 | # flake8 setup.py 19 | 20 | [testenv:lint] 21 | deps = {[testenv]deps} 22 | pylint 23 | commands = 24 | pylint --rcfile=.pylintrc pushmanager 25 | pylint --rcfile=.pylintrc pushmanager/tests 26 | 27 | [testenv:cover] 28 | deps = {[testenv:py]deps} 29 | commands = 30 | coverage erase 31 | coverage run -m testify.test_program {posargs:pushmanager.tests} 32 | coverage combine 33 | coverage report --omit=.tox/*,tests/*,/usr/share/pyshared/*,/usr/lib/pymodules/* -m 34 | 35 | [testenv:docs] 36 | deps = {[testenv:py]deps} 37 | sphinx 38 | changedir = docs 39 | commands = sphinx-build -b html -d build/doctrees source build/html 40 | 41 | 42 | [testenv:devenv] 43 | envdir = virtualenv_run 44 | commands = 45 | -------------------------------------------------------------------------------- /pushmanager/tests/test_ui_methods.py: -------------------------------------------------------------------------------- 1 | import testify as T 2 | from pushmanager.ui_methods import authorized_to_manage_request 3 | 4 | 5 | class UIMethodTest(T.TestCase): 6 | 7 | def test_authorized_to_manage_request_random_user(self): 8 | request = {'user': 'testuser', 'watchers': None } 9 | T.assert_equal(False, authorized_to_manage_request(None, request, 'notme')) 10 | 11 | def test_authorized_to_manage_request_request_user(self): 12 | request = {'user': 'testuser', 'watchers': None } 13 | T.assert_equal(True, authorized_to_manage_request(None, request, 'testuser')) 14 | 15 | def test_authorized_to_manage_request_pushmaster(self): 16 | request = {'user': 'testuser', 'watchers': None } 17 | T.assert_equal(True, authorized_to_manage_request(None, request, 'notme', True)) 18 | 19 | def test_authorized_to_manage_request_watcher(self): 20 | request = {'user': 'testuser', 'watchers': 'watcher1' } 21 | T.assert_equal(True, authorized_to_manage_request(None, request, 'watcher1')) 22 | 23 | 24 | if __name__ == '__main__': 25 | T.run() 26 | -------------------------------------------------------------------------------- /pushmanager/servlets/pushes.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class PushesServlet(RequestHandler): 8 | 9 | @tornado.web.authenticated 10 | @tornado.web.asynchronous 11 | @tornado.gen.engine 12 | def get(self): 13 | pushes_per_page = pushmanager.core.util.get_int_arg(self.request, 'rpp', 50) 14 | before = pushmanager.core.util.get_int_arg(self.request, 'before', 0) 15 | response = yield tornado.gen.Task( 16 | self.async_api_call, 17 | "pushes", 18 | {"rpp": pushes_per_page, "before": before} 19 | ) 20 | 21 | results = self.get_api_results(response) 22 | if not results: 23 | self.finish() 24 | 25 | pushes, last_push = results 26 | self.render( 27 | "pushes.html", 28 | page_title="Pushes", 29 | pushes=pushes, 30 | rpp=pushes_per_page, 31 | last_push=last_push 32 | ) 33 | -------------------------------------------------------------------------------- /pushmanager/templates/edit-push.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /pushmanager/templates/push.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}{{ escape(push_info['user']) }} \ {{ escape(push_info['title']) }}{% end %} 4 | 5 | {% block content %} 6 |
  7 | {% include 'edit-push.html' %} 8 |
9 |
  10 | {% include 'confirm-conflict-check.html' %} 11 |
12 | 13 | {% include 'push-info.html' %} 14 | 15 | {% include 'push-button-bar.html' %} 16 | 17 | {% include 'push-status.html' %} 18 | 19 | {% include 'push-dialogs.html' %} 20 | 21 | {% end %} 22 | 23 | {% block scripts %} 24 | 25 | 35 | {% end %} 36 | -------------------------------------------------------------------------------- /pushmanager/servlets/summaryforbranch.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | import pushmanager.core.util 4 | import tornado.gen 5 | import tornado.web 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.settings import Settings 8 | 9 | 10 | class SummaryForBranchServlet(RequestHandler): 11 | 12 | @tornado.web.asynchronous 13 | @tornado.gen.engine 14 | def get(self): 15 | userbranch = pushmanager.core.util.get_str_arg(self.request, 'userbranch') 16 | user, branch = userbranch.split('/', 1) 17 | response = yield tornado.gen.Task( 18 | self.async_api_call, 19 | "requestsearch", 20 | {'repo': user, 'branch': branch} 21 | ) 22 | 23 | requests = self.get_api_results(response) 24 | 25 | if requests: 26 | req = sorted(requests, key=operator.itemgetter("id"))[0] 27 | self.write(req['description'] or req['title']) 28 | if req['reviewid']: 29 | self.write("\n\nReview: https://%s/r/%s" % (Settings['reviewboard']['servername'], req['reviewid'])) 30 | self.finish() 31 | else: 32 | self.send_error(404) 33 | -------------------------------------------------------------------------------- /pushmanager/servlets/requests.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class RequestsServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.web.authenticated 11 | @tornado.gen.engine 12 | def get(self): 13 | username = pushmanager.core.util.get_str_arg(self.request, 'user') 14 | limit_count = pushmanager.core.util.get_int_arg(self.request, 'max') 15 | arguments = {'limit' : limit_count} 16 | 17 | if username: 18 | arguments['user'] = username 19 | page_title = 'Requests from %s' % username 20 | show_count = False 21 | else: 22 | arguments['limit'] = 0 23 | arguments['state'] = 'requested' 24 | page_title = 'Open Requests' 25 | show_count = True 26 | 27 | response = yield tornado.gen.Task( 28 | self.async_api_call, 29 | "requestsearch", 30 | arguments 31 | ) 32 | 33 | requests = self.get_api_results(response) 34 | self.render("requests.html", requests=requests, page_title=page_title, show_count=show_count) 35 | -------------------------------------------------------------------------------- /pushmanager/servlets/undelayrequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | import tornado.web 6 | from pushmanager.core.requesthandler import RequestHandler 7 | 8 | 9 | class UndelayRequestServlet(RequestHandler): 10 | 11 | @tornado.web.asynchronous 12 | def post(self): 13 | if not self.current_user: 14 | return self.send_error(403) 15 | self.requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 16 | update_query = db.push_requests.update().where(SA.and_( 17 | db.push_requests.c.id == self.requestid, 18 | db.push_requests.c.user == self.current_user, 19 | db.push_requests.c.state == 'delayed', 20 | )).values({ 21 | 'state': 'requested', 22 | }) 23 | select_query = db.push_requests.select().where( 24 | db.push_requests.c.id == self.requestid, 25 | ) 26 | db.execute_transaction_cb([update_query, select_query], self.on_db_complete) 27 | 28 | # allow both GET and POST 29 | get = post 30 | 31 | def on_db_complete(self, success, db_results): 32 | self.check_db_results(success, db_results) 33 | 34 | self.redirect("/requests?user=%s" % self.current_user) 35 | -------------------------------------------------------------------------------- /pushmanager/templates/modules/request-info.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
  • {{ escape(request['user']) }}{% if request['watchers'] %} ({{ escape(request['watchers']) }}){% end %}
  • 4 | 5 | {% if show_ago %} 6 |
  • 7 | {% if request['state'] in ('discarded', 'live') %} 8 | {{ pretty_date(int(request['modified'])) }} 9 | {% else %} 10 | {{ pretty_date(int(request['created'])) }} 11 | {% end %} 12 |
  • 13 | {% end %} 14 | 15 |
  • {{ escape(request['title']) }}
  • 16 | 17 | {% if tags %} 18 |
    • 19 | {% for (tag, tag_url) in tags %}
    • 20 | {% if tag_url %}{{ escape(tag) }}{% else %}{{ escape(tag) }}{% end %} 21 |
    • {% end %} 22 |
  • 23 | {% end %} 24 | 25 | {% if show_state_inline %} 26 |
  • 27 | {% if request['state'] in ('added','staged','verified', 'blessed', 'live') %} 28 | {{ escape(request['state']) }} 29 | {% else %} 30 | {{ escape(request['state']) }} 31 | {% end %} 32 |
  • 33 | {% end %} 34 | 35 |
36 | -------------------------------------------------------------------------------- /pushmanager/core/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | import sys 5 | 6 | import yaml 7 | from pushmanager.core.util import dict_copy_keys 8 | 9 | 10 | configuration_file = os.environ.get('SERVICE_ENV_CONFIG_PATH') 11 | 12 | Settings = {} 13 | 14 | try: 15 | with open(configuration_file) as settings_yaml: 16 | Settings = yaml.safe_load(settings_yaml) 17 | except: 18 | logging.error("Can not load configuration from '%s'." % configuration_file) 19 | sys.exit(1); 20 | 21 | # JS files in static/js need to know some of the configuration options 22 | # too, but we do not have to export everything, just what's 23 | # needed. This is what's needed. We're only setting up/defining keys 24 | # here and will copy values from Settings. 25 | JSSettings = { 26 | 'main_app': { 27 | 'servername': None, 28 | 'port': None, 29 | }, 30 | 'buildbot': { 31 | 'servername': None, 32 | }, 33 | 'reviewboard': { 34 | 'servername': None, 35 | }, 36 | 'ticket_tracker_url_format': None, 37 | 'git': { 38 | 'main_repository': None, 39 | }, 40 | 'check_sites_bookmarklet': None, 41 | } 42 | 43 | dict_copy_keys(to_dict=JSSettings, from_dict=Settings) 44 | 45 | __all__ = ['Settings', 'JSSettings'] 46 | -------------------------------------------------------------------------------- /pushmanager/templates/modules/request-buttons.html: -------------------------------------------------------------------------------- 1 | {% if push_buttons %} 2 | 3 | 4 | {% if pushmaster %} 5 | 6 | 7 | 8 | 9 | 10 | {% end %} 11 | 12 | {% if authorized_to_manage_request(request, current_user, pushmaster) %} 13 | 14 | 15 | 16 | 17 | {% end %} 18 | 19 | {% elif edit_buttons %} 20 | 21 | 22 | {% if request['user'] == current_user %} 23 | 24 | {% if request['state'] == 'requested' %} 25 | 26 | {% elif request['state'] == 'delayed' %} 27 | 28 | {% end %} 29 | 30 | {% if request['state'] in ('requested', 'delayed') %} 31 | 32 | {% end %} 33 | 34 | {% end %} 35 | 36 | {% end %} 37 | -------------------------------------------------------------------------------- /pushmanager/servlets/smartdest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import tornado.web 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class SmartDestServlet(RequestHandler): 9 | 10 | @tornado.web.authenticated 11 | def get(self): 12 | query = db.push_pushes.select(SA.and_( 13 | db.push_pushes.c.state == 'accepting', 14 | SA.exists([1], 15 | SA.and_( 16 | db.push_pushcontents.c.push == db.push_pushes.c.id, 17 | db.push_pushcontents.c.request == db.push_requests.c.id, 18 | db.push_requests.c.user == self.current_user, 19 | ), 20 | ), 21 | ), 22 | order_by=db.push_pushes.c.created.asc(), 23 | ) 24 | db.execute_cb(query, self.on_db_response) 25 | 26 | def on_db_response(self, success, db_results): 27 | self.check_db_results(success, db_results) 28 | 29 | if db_results and db_results.rowcount > 0: 30 | push = db_results.first() 31 | if push: 32 | return self.redirect('/push?id=%s' % push['id']) 33 | 34 | return self.redirect('/pushes') 35 | -------------------------------------------------------------------------------- /pushmanager/core/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | 5 | import ldap 6 | 7 | from pushmanager.core.settings import Settings 8 | 9 | 10 | os.environ['LDAPTLS_REQCERT'] = 'demand' 11 | os.environ['LDAPTLS_CACERT'] = Settings['auth_ldap']['cert_file'] 12 | 13 | LDAP_URL = Settings['auth_ldap']['url'] 14 | 15 | def authenticate(username, password): 16 | """Attempts to bind a given username/password pair in LDAP and returns whether or not it succeeded.""" 17 | try: 18 | dn = "%s@%s" % (username, Settings['auth_ldap']['domain']) 19 | basedn = Settings['auth_ldap']['basedn'] 20 | 21 | con = ldap.initialize(LDAP_URL) 22 | 23 | con.set_option(ldap.OPT_NETWORK_TIMEOUT, 3) 24 | con.set_option(ldap.OPT_REFERRALS, 0) 25 | con.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) 26 | 27 | con.start_tls_s() 28 | try: 29 | con.simple_bind_s(dn, password) 30 | con.search_s(basedn, ldap.SCOPE_ONELEVEL) 31 | except: 32 | return False 33 | con.unbind_s() 34 | return True 35 | except: 36 | # Tornado will log POST data in case of an uncaught 37 | # exception. In this case POST data will have username & 38 | # password and we do not want it. 39 | logging.exception("Authentication error") 40 | return False 41 | 42 | __all__ = ['authenticate'] 43 | -------------------------------------------------------------------------------- /pushmanager/pushmanager_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import with_statement 3 | 4 | import os 5 | 6 | import pushmanager.core.db as db 7 | import tornado.httpserver 8 | import tornado.process 9 | import pushmanager.ui_modules as ui_modules 10 | from pushmanager.core.application import Application 11 | from pushmanager.core.settings import Settings 12 | from pushmanager.core.util import get_servlet_urlspec 13 | from pushmanager.servlets.api import APIServlet 14 | 15 | 16 | api_application = tornado.web.Application( 17 | # Servlet dispatch rules 18 | [ 19 | get_servlet_urlspec(APIServlet), 20 | ], 21 | # Server settings 22 | static_path = os.path.join(os.path.dirname(__file__), "static"), 23 | template_path = os.path.join(os.path.dirname(__file__), "templates"), 24 | gzip = True, 25 | login_url = "/login", 26 | cookie_secret = Settings['cookie_secret'], 27 | ui_modules = ui_modules, 28 | autoescape = None, 29 | ) 30 | 31 | 32 | class PushManagerAPIApp(Application): 33 | name = "api" 34 | 35 | def start_services(self): 36 | # HTTP server (for api) 37 | sockets = tornado.netutil.bind_sockets(self.port, address=Settings['api_app']['servername']) 38 | tornado.process.fork_processes(Settings['tornado']['num_workers']) 39 | server = tornado.httpserver.HTTPServer(api_application) 40 | server.add_sockets(sockets) 41 | 42 | 43 | def main(): 44 | app = PushManagerAPIApp() 45 | db.init_db() 46 | app.run() 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /pushmanager/templates/push-button-bar.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 |
  • 4 | — 5 | {% if current_user in (push_info['extra_pings'] or []) %} 6 |
  • 7 | {% else %} 8 |
  • 9 | {% end %} 10 | {% if push_info['user'] == current_user or override %} 11 | — 12 |
  • 13 | {% if push_info['state'] == 'accepting' %} 14 |
  • 15 | — 16 |
  • 17 |
  • 18 |
  • 19 | — 20 |
  • 21 |
  • 22 |
  • 23 | — 24 |
  • 25 |
  • 26 |
  • 27 | {% end %} 28 | {% else %} 29 | {% if push_info['state'] == 'accepting' %} 30 | —
  • 31 | {% end %} 32 | {% end %} 33 |
  • {{ modules.NewRequestDialog() }}
  • 34 |
35 | -------------------------------------------------------------------------------- /pushmanager/servlets/push.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pushmanager.core.util 4 | import tornado.gen 5 | import tornado.web 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.settings import Settings 8 | 9 | 10 | def _repo(base): 11 | dev_repos_dir = Settings['git']['dev_repositories_dir'] 12 | main_repository = Settings['git']['main_repository'] 13 | return os.path.join(dev_repos_dir, base) if base != main_repository else base 14 | 15 | class PushServlet(RequestHandler): 16 | 17 | @tornado.web.asynchronous 18 | @tornado.web.authenticated 19 | @tornado.gen.engine 20 | def get(self): 21 | pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 22 | override = pushmanager.core.util.get_int_arg(self.request, 'override') 23 | response = yield tornado.gen.Task( 24 | self.async_api_call, 25 | "pushdata", 26 | {"id": pushid} 27 | ) 28 | 29 | push_info, push_requests, available_requests = self.get_api_results(response) 30 | 31 | if not push_info['stageenv']: 32 | push_info['stageenv'] = '(to be determined)' 33 | 34 | push_survey_url = Settings.get('push_survey_url', None) 35 | 36 | self.render( 37 | "push.html", 38 | page_title=push_info['title'], 39 | pushid=pushid, 40 | push_info=push_info, 41 | push_contents=push_requests, 42 | push_survey_url=push_survey_url, 43 | available_requests=available_requests, 44 | fullrepo=_repo, 45 | override=override 46 | ) 47 | -------------------------------------------------------------------------------- /pushmanager/templates/check_sites_bookmarklet.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | Settings = {{ JSSettings_json }}; 3 | 4 | var domainName = Settings['check_sites_bookmarklet']['domain_name']; 5 | var urls = Settings['check_sites_bookmarklet']['urls']; 6 | // 'substitutions' defines strings that need to be replaced for 7 | // non-production environments. Example object for this: 8 | // {'prod_id': 'dev_id'} 9 | // The example above will replace 'prod_id' with 'dev_id' in the 10 | // given list of urls. 11 | var substitutions = Settings['check_sites_bookmarklet']['substitutions'] || {}; 12 | 13 | var prod = 'prod'; 14 | 15 | var env = window.prompt('Which environment would you like to test? e.g.: prod, stagea, stageb.', prod); 16 | 17 | // If env is false, the user hit 'cancel', and let's abort. 18 | if (!env) { 19 | return; 20 | } 21 | 22 | for (var i=0; i < urls.length; ++i) { 23 | var url = urls[i]; 24 | 25 | // We assume all URLs are encoded against a prod environment. 26 | // If not, we modify the URL with what we assume to be a testing sub-domain. e.g.: foo.com -> stage.foo.com 27 | if (env !== prod) { 28 | 29 | $.each(substitutions, function(prodString, devString) { 30 | if (url.match(prodString)) { 31 | url = url.replace(prodString, devString); 32 | } 33 | }); 34 | 35 | url = url.replace(domainName, env + '.' + domainName); 36 | } 37 | 38 | url = 'http://' + url; 39 | 40 | window.open(url, url, 'resizable=yes,menubar=yes,toolbar=yes,scrollbars=yes,status=yes,location=yes'); 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /pushmanager/servlets/pingme.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.db as db 2 | import pushmanager.core.util 3 | import tornado.gen 4 | import tornado.web 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class PingMeServlet(RequestHandler): 9 | 10 | @tornado.web.authenticated 11 | @tornado.web.asynchronous 12 | @tornado.gen.engine 13 | def get(self): 14 | pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 15 | ping_action = pushmanager.core.util.get_str_arg(self.request, 'action') 16 | response = yield tornado.gen.Task( 17 | self.async_api_call, 18 | "push", 19 | {"id": pushid} 20 | ) 21 | 22 | push = self.get_api_results(response) 23 | if not push: 24 | self.send_error() 25 | 26 | pings = set(x for x in (push['extra_pings'] or "").split(',') if x) 27 | if ping_action == 'set': 28 | pings.add(self.current_user) 29 | else: 30 | pings.discard(self.current_user) 31 | 32 | # This is not atomic, so we could theoretically 33 | # run into race conditions here, but since we're 34 | # working at machine speed on human input triggers 35 | # it should be okay for now. 36 | query = db.push_pushes.update().where( 37 | db.push_pushes.c.id == pushid, 38 | ).values({ 39 | 'extra_pings': ','.join(pings), 40 | }) 41 | db.execute_cb(query, self.on_update_complete) 42 | 43 | def on_update_complete(self, success, db_results): 44 | self.check_db_results(success, db_results) 45 | self.finish() 46 | -------------------------------------------------------------------------------- /pushmanager/tests/test_servlet_pushitems.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | 4 | import mock 5 | import testify as T 6 | from pushmanager.core.util import get_servlet_urlspec 7 | from pushmanager.servlets.pushitems import PushItemsServlet 8 | from pushmanager.testing.testservlet import ServletTestMixin 9 | 10 | 11 | class PushsItemsServletTest(T.TestCase, ServletTestMixin): 12 | 13 | def get_handlers(self): 14 | return [ 15 | get_servlet_urlspec(PushItemsServlet), 16 | ] 17 | 18 | @T.setup 19 | def setup_fake_requests_response(self): 20 | self.fake_request_data = { 21 | "created": 1346458663.2721, 22 | "title": "One Push", 23 | "modified": 1346458663.2721, 24 | "tags": "fake_tag1,fake_tag2", 25 | "user": "drseuss", 26 | "repo": "drseuss", 27 | "comments": "No comment", 28 | "branch": "fake_branchname", 29 | "reviewid": 10, 30 | "id": 1 31 | } 32 | self.fake_requests_response = "[%s]" % json.dumps(self.fake_request_data) 33 | 34 | 35 | def test_pushitems(self): 36 | with contextlib.nested( 37 | mock.patch.object(PushItemsServlet, "get_current_user", return_value=self.fake_request_data["user"]), 38 | mock.patch.object(PushItemsServlet, "async_api_call", side_effect=self.mocked_api_call), 39 | mock.patch.object(self, "api_response", return_value=self.fake_requests_response) 40 | ): 41 | self.fetch("/pushitems?push=%d" % self.fake_request_data["id"]) 42 | response = self.wait() 43 | T.assert_in(self.fake_request_data["title"], response.body) 44 | -------------------------------------------------------------------------------- /pushmanager/tests/test_bookmarklet.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | import mock 4 | import testify as T 5 | from pushmanager.handlers import CheckSitesBookmarkletHandler 6 | from pushmanager.handlers import CreateRequestBookmarkletHandler 7 | from pushmanager.testing.testservlet import AsyncTestCase 8 | 9 | 10 | class BookmarkletTest(T.TestCase, AsyncTestCase): 11 | 12 | def get_handlers(self): 13 | return [ 14 | (CreateRequestBookmarkletHandler.url, CreateRequestBookmarkletHandler), 15 | (CheckSitesBookmarkletHandler.url, CheckSitesBookmarkletHandler), 16 | ] 17 | 18 | @contextlib.contextmanager 19 | def page(self, handler): 20 | with mock.patch.object(handler, "get_current_user"): 21 | handler.get_current_user.return_value = "testuser" 22 | response = self.fetch(str(handler.url)) 23 | yield response 24 | 25 | def test_create_request_bookmarklet(self): 26 | with self.page(CreateRequestBookmarkletHandler) as response: 27 | # We'll get a javascript as the body, just check some 28 | # variable names/strings that we know is there in the 29 | # script. 30 | T.assert_equal(response.error, None) 31 | T.assert_in("ticketNumberToURL", response.body) 32 | T.assert_in("codeReview", response.body) 33 | T.assert_in("%TICKET%", response.body) 34 | 35 | 36 | def test_check_sites_bookmarklet(self): 37 | with self.page(CheckSitesBookmarkletHandler) as response: 38 | # See comment above in test_create_request_bookmarklet 39 | T.assert_equal(response.error, None) 40 | T.assert_in("window.open", response.body) 41 | -------------------------------------------------------------------------------- /pushmanager/templates/push-info.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • Pushmaster{{ escape(push_info['user']) }}
  • 3 |
  • Branch{{ escape(push_info['branch']) }}
  • 4 | {% if push_info['stageenv'] %}
  • Stage{{ escape(push_info['stageenv']) }}
  • {% end %} 5 | {% if push_info['state'] == 'accepting' %} 6 |
  • Buildbot Runsurl
  • 7 | {% end %} 8 |
  • State
      9 |
    • {{ escape(push_info['state']) }}
    • 10 |
  • 11 |
  • Push Type
      12 |
    • {{ escape(push_info['pushtype']) }}
    • 13 |
  • 14 |
  • Created{{ datetime.datetime.fromtimestamp(push_info['created']).strftime("%x %X") }}
  • 15 | {% if not push_info['created'] == push_info['modified'] %} 16 |
  • Modified{{ datetime.datetime.fromtimestamp(push_info['modified']).strftime("%x %X") }}
  • 17 | {% end %} 18 |
19 | -------------------------------------------------------------------------------- /pushmanager/templates/pushitems.html: -------------------------------------------------------------------------------- 1 | {% for request in requests %} 2 |
  • 3 | 4 |
      5 |
    • From 6 | {{ escape(request['user']) }}
    • 7 |
    • {{ escape(request['title']) }}
    • 8 | {% if request['tags'] %} 9 |
    • Tags 10 |
        11 | {% for tag in request['tags'].split(',') %}
      • {{ escape(tag) }}
      • {% end %} 12 |
    • 13 | {% end %} 14 |
    15 |
    16 |
      17 | {% if request['reviewid'] %} 18 |
    • Review # 19 | 20 | {{ int(request['reviewid']) }} 21 |
    • 22 | {% end %} 23 |
    • Repo{{ escape(request['repo']) }}
    • 24 |
    • Branch{{ escape(request['branch']) }}
    • 25 |
    • Created{{ datetime.datetime.fromtimestamp(request['created']).strftime("%x %X") }}
    • 26 | {% if not request['created'] == request['modified'] %} 27 |
    • Modified{{ datetime.datetime.fromtimestamp(request['modified']).strftime("%x %X") }}
    • 28 | {% end %} 29 | {% if request['comments'] %} 30 |
      {{ escape(request['comments']).replace('\n', '
      ') }}
      31 | {% end %} 32 |
    33 |
    34 |
  • 35 | {% end %} 36 | -------------------------------------------------------------------------------- /pushmanager/servlets/msg.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from pushmanager.core.requesthandler import RequestHandler 4 | from pushmanager.core.settings import Settings 5 | 6 | 7 | class MsgServlet(RequestHandler): 8 | 9 | def post(self): 10 | if not self.current_user: 11 | return self.send_error(403) 12 | 13 | people = self.request.arguments.get('people[]', []) 14 | message = self.request.arguments.get('message', [None])[0] 15 | if not message: 16 | return self.send_error(500) 17 | 18 | irc_nick = Settings['irc']['nickname'].format( 19 | pushmaster=self.current_user 20 | ) 21 | 22 | if not people: 23 | irc_message = u'[[pushmaster {0}]] {1}'.format( 24 | self.current_user, 25 | message, 26 | ) 27 | 28 | subprocess.call([ 29 | '/nail/sys/bin/nodebot', 30 | '-i', 31 | irc_nick, 32 | Settings['irc']['channel'], 33 | irc_message 34 | ]) 35 | return 36 | 37 | # divide people into groups, each group has 5 persons. 38 | groups = [people[i:i+5] for i in range(0, len(people), 5)] 39 | 40 | for i, group in enumerate(groups): 41 | irc_message = u'{0} {1}{2}'.format( 42 | '[[pushmaster %s]]' % self.current_user if not i else '', 43 | ', '.join(group), 44 | ': ' + message if i == len(groups) - 1 else '', 45 | ) 46 | subprocess.call([ 47 | '/nail/sys/bin/nodebot', 48 | '-i', 49 | irc_nick, 50 | Settings['irc']['channel'], 51 | irc_message 52 | ]) 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import find_packages 5 | from setuptools import setup 6 | 7 | from pushmanager.__about__ import __version__ 8 | 9 | 10 | setup( 11 | name='pushmanager', 12 | version=__version__, 13 | provides=['pushmanager'], 14 | author='Yelp', 15 | author_email='yelplabs@yelp.com', 16 | url='https://github.com/Yelp/pushmanager', 17 | description='Deployment managing system', 18 | classifiers=[ 19 | "Programming Language :: Python", 20 | 'Programming Language :: Python :: 2.5', 21 | 'Programming Language :: Python :: 2.6', 22 | "Operating System :: OS Independent", 23 | "License :: OSI Approved :: Apache Software License", 24 | "Development Status :: 4 - Beta", 25 | "Topic :: Software Development :: Build Tools", 26 | ], 27 | license='Copyright Yelp 2013', 28 | packages=find_packages(exclude=["tests"]), 29 | include_package_data=True, 30 | entry_points = { 31 | 'console_scripts': [ 32 | 'pushmanager_api = pushmanager.pushmanager_api:main', 33 | 'pushmanager_main = pushmanager.pushmanager_main:main', 34 | ], 35 | }, 36 | setup_requires=['setuptools'], 37 | install_requires=[ 38 | 'lxml == 2.2.4', 39 | 'mysql-python == 1.2.5', 40 | 'python-daemon == 1.5.2', 41 | 'python-ldap == 2.4.13', 42 | 'tornado == 2.4.1', 43 | 'xmpppy == 0.5.0rc1', 44 | ], 45 | long_description="""Pushmanager is a tornado web application we use to manage deployments at Yelp. It helps pushmasters to conduct the deployment by bringing together push requests from engineers and information gathered from reviews, test builds and issue tracking system.""", 46 | ) 47 | -------------------------------------------------------------------------------- /pushmanager/templates/checklist.html: -------------------------------------------------------------------------------- 1 | {% if not any(items_by_target.values()) %} 2 | (No checklist items for this push.) 3 | {% else %} 4 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Staging - Do In Stage', target='stage') %} 5 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Staging - Do In Stage', target='post-stage') %} 6 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Blessing - Do In Prod', target='prod') %} 7 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Blessing - Do In Prod', target='post-prod') %} 8 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Certifying - Do In Dev', target='verify') %} 9 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Certifying - Do In Prod', target='post-verify-prod') %} 10 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Certifying - Do In Dev', target='post-verify') %} 11 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Certifying - Do In Stage', target='post-verify-stage') %} 12 | {% end %} 13 | 14 | -------------------------------------------------------------------------------- /pushmanager/tests/test_servlet_pushes.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | import lxml.html 4 | 5 | import mock 6 | import testify as T 7 | from pushmanager.core.util import get_servlet_urlspec 8 | from pushmanager.servlets.pushes import PushesServlet 9 | from pushmanager.testing.testservlet import ServletTestMixin 10 | 11 | 12 | class PushesServletTest(T.TestCase, ServletTestMixin): 13 | 14 | def get_handlers(self): 15 | return [ 16 | get_servlet_urlspec(PushesServlet), 17 | ] 18 | 19 | def find_push_in_response(self, response, title): 20 | assert response.error is None 21 | root = lxml.html.fromstring(response.body) 22 | for elt in root.xpath("//div[@class='push-header']/a"): 23 | if elt.text.startswith(title): 24 | return True 25 | return False 26 | 27 | def test_pushes(self): 28 | with contextlib.nested( 29 | mock.patch.object(PushesServlet, "get_current_user"), 30 | mock.patch.object(PushesServlet, "async_api_call"), 31 | mock.patch.object(self, "api_response") 32 | ): 33 | PushesServlet.get_current_user.return_value = "testuser" 34 | PushesServlet.async_api_call.side_effect = self.mocked_api_call 35 | one_push = """{ 36 | "created": 1346458663.2721, 37 | "title": "One Push", 38 | "modified": 1346458663.2721, 39 | "state": "accepting", 40 | "user": "drseuss", 41 | "branch": "deploy-second", 42 | "extra_pings": null, 43 | "pushtype": "private", 44 | "id": 1 45 | }""" 46 | self.api_response.return_value = "[[%s], 1]" % one_push 47 | self.fetch("/pushes") 48 | response = self.wait() 49 | T.assert_equal(self.find_push_in_response(response, "One Push"), True) 50 | -------------------------------------------------------------------------------- /pushmanager/templates/create_request_bookmarklet.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | Settings = {{ JSSettings_json }}; 3 | 4 | var ticketNumberToURL = function(bug) { 5 | return Settings['ticket_tracker_url_format'].replace("%TICKET%", bug); 6 | }; 7 | 8 | var summary = $('#summary').text(); 9 | var codeReview = location.href.split('#')[0]; 10 | var reviewid = codeReview.match(/\d+/)[0]; 11 | var tickets = $('#bugs_closed').text().split(',').filter(Boolean).map(ticketNumberToURL); 12 | var description = summary + '\n\n' + $('#description').text(); 13 | 14 | // Get a list of reviewers who have a 'Ship it!', filtering out dupes 15 | var reviewerSet = {}; 16 | var reviewers = $('div.shipit ~ div.reviewer > a').map(function() { 17 | var reviewer = $.trim(this.text); 18 | if (reviewer && !reviewerSet[reviewer]) { 19 | reviewerSet[reviewer] = true; 20 | return reviewer; 21 | } 22 | }).get(); 23 | 24 | var branch = $('#branch').text(); 25 | var repo = Settings['git']['main_repository']; 26 | if(branch.indexOf('/') != -1) { 27 | var branchparts = branch.split('/', 2); 28 | repo = branchparts[0]; 29 | branch = branchparts[1]; 30 | } 31 | 32 | var comments = []; 33 | if (reviewers.length > 0) { 34 | comments.push('SheepIt from ' + reviewers.join(', ')); 35 | } 36 | if (tickets.length > 0) { 37 | comments.push((tickets.length == 1 ? 'Ticket: ' : 'Tickets: ') + tickets.join(' ')); 38 | } 39 | comments = comments.join('\n\n'); 40 | 41 | main_app_port = Settings['main_app']['port'] == 443 ? ':' + Settings['main_app']['port'] : ''; 42 | 43 | location.href = 'https://' + Settings['main_app']['servername'] + main_app_port + '/requests?' + $.param({ 44 | 'bookmarklet': 1, 45 | 'title': summary, 46 | 'repo': repo, 47 | 'branch': branch, 48 | 'review': reviewid, 49 | 'comments': comments, 50 | 'description': description 51 | }); 52 | })(); 53 | -------------------------------------------------------------------------------- /tools/rename_checklist_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Renames a checklist type for all checklists in the database. 4 | 5 | A checklist type may have two forms - name and name-cleanup. 6 | 7 | With an appropriate config.yaml running from the root of the pushmanager-service: 8 | python -u tools/rename_checklist_type.py oldname newname 9 | 10 | Reverting the change (if tags are unique) is as simple as swapping the tags. 11 | 12 | Note: 13 | 14 | Checklist type renames will mostly require code changes. 15 | """ 16 | import sys 17 | from functools import partial 18 | from optparse import OptionParser 19 | 20 | import pushmanager.core.db as db 21 | 22 | 23 | def main(): 24 | usage = 'usage: %prog ' 25 | parser = OptionParser(usage) 26 | (_, args) = parser.parse_args() 27 | 28 | if len(args) == 2: 29 | db.init_db() 30 | convert_checklist(args[0], args[1]) 31 | db.finalize_db() 32 | else: 33 | parser.error('Incorrect number of arguments') 34 | 35 | def convert_checklist(old, new): 36 | print 'Renaming %s to %s in checklist types' % (old, new) 37 | 38 | cb = partial(convert_checklist_callback, old, new) 39 | 40 | cselect_query = db.push_checklist.select() 41 | db.execute_transaction_cb([cselect_query], cb) 42 | 43 | 44 | def convert_checklist_callback(old, new, success, db_results): 45 | check_db_results(success, db_results) 46 | 47 | checklists = db_results[0].fetchall() 48 | 49 | convert = { 50 | old: new, 51 | '%s-cleanup' % old: '%s-cleanup' % new 52 | } 53 | 54 | update_queries = [] 55 | for checklist in checklists: 56 | if checklist['type'] in convert.keys(): 57 | update_query = db.push_checklist.update().where( 58 | db.push_checklist.c.id == checklist.id 59 | ).values({'type': convert[checklist['type']]}) 60 | update_queries.append(update_query) 61 | 62 | db.execute_transaction_cb(update_queries, check_db_results) 63 | 64 | def check_db_results(success, db_results): 65 | if not success: 66 | raise db.DatabaseError() 67 | 68 | 69 | if __name__ == '__main__': 70 | sys.exit(main()) 71 | -------------------------------------------------------------------------------- /pushmanager/servlets/discardpush.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import sqlalchemy as SA 4 | 5 | import pushmanager.core.db as db 6 | import pushmanager.core.util 7 | from pushmanager.core.requesthandler import RequestHandler 8 | 9 | 10 | class DiscardPushServlet(RequestHandler): 11 | 12 | def _arg(self, key): 13 | return pushmanager.core.util.get_str_arg(self.request, key, '') 14 | 15 | def post(self): 16 | if not self.current_user: 17 | return self.send_error(403) 18 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 19 | push_query = db.push_pushes.update().where(db.push_pushes.c.id == self.pushid).values({ 20 | 'state': 'discarded', 21 | 'modified': time.time(), 22 | }) 23 | request_query_pickme = db.push_requests.update().where( 24 | SA.exists([1], 25 | SA.and_( 26 | db.push_pushcontents.c.push == self.pushid, 27 | db.push_pushcontents.c.request == db.push_requests.c.id, 28 | db.push_requests.c.state == 'pickme', 29 | ) 30 | )).values({ 31 | 'state': 'requested', 32 | }) 33 | delete_query = db.push_pushcontents.delete().where( 34 | SA.exists([1], 35 | SA.and_( 36 | db.push_pushcontents.c.push == self.pushid, 37 | db.push_pushcontents.c.request == db.push_requests.c.id, 38 | db.push_requests.c.state == 'requested', 39 | ) 40 | )) 41 | request_query_all = db.push_requests.update().where( 42 | SA.exists([1], 43 | SA.and_( 44 | db.push_pushcontents.c.push == self.pushid, 45 | db.push_pushcontents.c.request == db.push_requests.c.id, 46 | ) 47 | )).values({ 48 | 'state': 'requested', 49 | }) 50 | db.execute_transaction_cb([push_query, request_query_pickme, delete_query, request_query_all], self.on_db_complete) 51 | 52 | def on_db_complete(self, success, db_results): 53 | self.check_db_results(success, db_results) 54 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.3.6 2014-08-22 2 | 3 | BUG FIXES 4 | 5 | * Track worker pids (rschlaikjer, #98) 6 | * Skip pickmes in conlfict detection if they are no longer valid (rschlaikjer, #101, #102) 7 | 8 | IMPROVEMENTS 9 | 10 | * Include stdout on conflict-master pickmes (rschlaikjer, #100) 11 | 12 | PERFORMANCE TWEAKS 13 | 14 | * Merge conflict detection uses multiple threads (rschlaikjer, #99) 15 | 16 | 0.3.5 2014-08-09 17 | 18 | BUG FIXES 19 | 20 | * Fetch the pickme branch being checked (rschlaikjer, #96) 21 | 22 | 0.3.4 2014-08-08 23 | 24 | BUG FIXES 25 | 26 | * When checking if a pickme has already been merged, use 27 | the pickme's id. (rschlaikjer, #94) 28 | 29 | 0.3.3 2014-08-08 30 | 31 | BUG FIXES 32 | 33 | * Don't run merge detection on merged requests (rschlaikjer, #84, #91) 34 | * Fetch non-ff branches (rschlaikjer, #85, #90) 35 | 36 | IMPROVEMENTS 37 | 38 | * Manually rerun merge detection (rschlaikjer, #86, #92) 39 | 40 | PERFORMANCE TWEAKS 41 | 42 | * Separate git sha retrieval from git merge conflict detection. 43 | (rschlaikjer, #82, #83) 44 | 45 | 0.3.2 2014-08-15 46 | 47 | PERFORMANCE TWEAKS 48 | 49 | * Only requeue existing conflicted pickme's when a pickme 50 | is removed. (rschlaikjer, #79, #80) 51 | 52 | 0.3.1 2014-08-14 53 | 54 | BUG FIXES 55 | 56 | * Ensure submodules are refreshed for merge conflict detection 57 | (rschlaikjer, #73, #75) 58 | * Ensure the proper remote is fetched for merge conflict detection 59 | (rschlaikjer, #74, #75) 60 | * Hard reset and clean pre-checkout to prevent checkout being blocked 61 | by unstaged changes (rschlaikjer, #76) 62 | * Force delete / recreate of test branches, to fix branches that 63 | might not have been cleaned up properly (rschlaikjer, #76) 64 | 65 | PERFORMANCE TWEAKS 66 | 67 | * For performance reasons, only perform a fetch when a new remote or 68 | pickme has been added (rschlaikjer, #76) 69 | 70 | 0.3.0 2014-08-12 71 | 72 | NEW FEATURES 73 | 74 | * Automatic merge conflict detection (rschlaikjer, #66) 75 | 76 | 0.2.1 2014-08-04 77 | 78 | IMPROVEMENTS 79 | 80 | * Add pushmaster name to IRC messages (asotille, #63) 81 | * Clean up imports (asotille) 82 | * New submodule-bump tag for requests that include submodule 83 | changes (jagg81, #60) 84 | 85 | DEPRECATED 86 | 87 | * l10n and l10n-only tags are no longer used (jagg81, #60) 88 | -------------------------------------------------------------------------------- /pushmanager/templates/push-dialogs.html: -------------------------------------------------------------------------------- 1 |