diff --git a/gratipay/utils/ghost.py b/gratipay/utils/ghost.py new file mode 100644 index 0000000000..ea0848aa3d --- /dev/null +++ b/gratipay/utils/ghost.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +PACKAGE_JSON = '''\ +{ + "name": "ghost", + "version": "1.0.0-alpha.21", + "description": "Just a blogging platform.", + "author": "Ghost Foundation", + "homepage": "http://ghost.org", + "keywords": [ + "ghost", + "blog", + "cms" + ], + "repository": { + "type": "git", + "url": "git://github.com/TryGhost/Ghost.git" + }, + "bugs": "https://github.com/TryGhost/Ghost/issues", + "contributors": "https://github.com/TryGhost/Ghost/graphs/contributors", + "license": "MIT", + "main": "./core/index", + "scripts": { + "start": "node index", + "test": "grunt validate --verbose", + "init": "yarn global add knex-migrator ember-cli grunt-cli && yarn install && grunt symlink && grunt init || true" + }, + "engines": { + "node": "^4.5.0 || ^6.9.0" + }, + "dependencies": { + "amperize": "0.3.4", + "archiver": "1.3.0", + "bcryptjs": "2.4.3", + "bluebird": "3.5.0", + "body-parser": "1.17.1", + "bookshelf": "0.10.3", + "brute-knex": "https://github.com/cobbspur/brute-knex/tarball/37439f56965b17d29bb4ff9b3f3222b2f4bd6ce3", + "bson-objectid": "1.1.5", + "chalk": "1.1.3", + "cheerio": "0.22.0", + "compression": "1.6.2", + "connect-slashes": "1.3.1", + "cookie-session": "1.2.0", + "cors": "2.8.3", + "csv-parser": "1.11.0", + "debug": "2.6.6", + "downsize": "0.0.8", + "express": "4.15.2", + "express-brute": "1.0.1", + "express-hbs": "1.0.4", + "extract-zip-fork": "1.5.1", + "fs-extra": "3.0.1", + "ghost-gql": "0.0.6", + "ghost-ignition": "2.8.11", + "ghost-storage-base": "0.0.1", + "glob": "5.0.15", + "gscan": "1.1.0", + "html-to-text": "3.2.0", + "icojs": "0.7.2", + "image-size": "0.5.2", + "intl": "1.2.5", + "intl-messageformat": "1.3.0", + "jsdom": "9.12.0", + "jsonpath": "0.2.11", + "knex": "0.12.9", + "knex-migrator": "2.0.16", + "lodash": "4.17.4", + "markdown-it": "8.3.1", + "markdown-it-footnote": "3.0.1", + "markdown-it-lazy-headers": "0.1.3", + "markdown-it-mark": "2.0.0", + "markdown-it-named-headers": "0.0.4", + "mobiledoc-dom-renderer": "0.6.5", + "moment": "2.18.1", + "moment-timezone": "0.5.13", + "multer": "1.3.0", + "mysql": "2.13.0", + "nconf": "0.8.4", + "netjet": "1.1.3", + "nodemailer": "0.7.1", + "oauth2orize": "1.8.0", + "passport": "0.3.2", + "passport-ghost": "2.3.1", + "passport-http-bearer": "1.0.1", + "passport-oauth2-client-password": "0.1.2", + "path-match": "1.2.4", + "rss": "1.2.2", + "sanitize-html": "1.14.1", + "semver": "5.3.0", + "simple-dom": "0.3.2", + "simple-html-tokenizer": "0.4.1", + "superagent": "3.5.2", + "unidecode": "0.1.8", + "uuid": "3.0.1", + "validator": "6.3.0", + "xml": "1.0.1" + }, + "optionalDependencies": { + "sqlite3": "3.1.8" + }, + "devDependencies": { + "grunt": "1.0.1", + "grunt-bg-shell": "2.3.3", + "grunt-cli": "1.2.0", + "grunt-contrib-clean": "1.0.0", + "grunt-contrib-compress": "1.3.0", + "grunt-contrib-copy": "1.0.0", + "grunt-contrib-jshint": "1.0.0", + "grunt-contrib-symlink": "^1.0.0", + "grunt-contrib-uglify": "2.0.0", + "grunt-contrib-watch": "1.0.0", + "grunt-cssnano": "2.1.0", + "grunt-docker": "0.0.11", + "grunt-express-server": "0.5.3", + "grunt-jscs": "3.0.1", + "grunt-mocha-cli": "2.1.0", + "grunt-mocha-istanbul": "5.0.2", + "grunt-shell": "1.3.1", + "grunt-subgrunt": "1.2.0", + "grunt-update-submodules": "0.4.1", + "istanbul": "0.4.5", + "jshint": "2.9.4", + "jshint-stylish": "2.2.1", + "matchdep": "1.0.1", + "minimist": "1.2.0", + "mocha": "3.3.0", + "nock": "9.0.13", + "rewire": "2.5.2", + "run-sequence": "1.2.2", + "should": "11.2.1", + "should-http": "0.1.1", + "sinon": "1.17.7", + "supertest": "3.0.0", + "tmp": "0.0.31" + }, + "greenkeeper": { + "ignore": [ + "glob", + "nodemailer", + "grunt", + "grunt-bg-shell", + "grunt-cli", + "grunt-contrib-clean", + "grunt-contrib-compress", + "grunt-contrib-copy", + "grunt-contrib-jshint", + "grunt-contrib-uglify", + "grunt-contrib-watch", + "grunt-docker", + "grunt-express-server", + "grunt-jscs", + "grunt-mocha-cli", + "grunt-mocha-istanbul", + "grunt-shell", + "grunt-subgrunt", + "grunt-update-submodules", + "sinon" + ] + } +}''' diff --git a/gratipay/utils/listings.py b/gratipay/utils/listings.py new file mode 100644 index 0000000000..76879cf466 --- /dev/null +++ b/gratipay/utils/listings.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + + +class FakeProject(object): + + def __init__(self, website, package): + self.website = website + self.package = package + self.name = package.name + self.url_path = '/on/{}/{}/'.format(package.package_manager, package.name) + + def get_image_url(self, size): + assert size in ('large', 'small'), size + return self.website.asset('package-default-{}.png'.format(size)) + + +def with_unclaimed_packages_wrapped(website, projects_and_unclaimed_packages): + out = [] + for project, unclaimed_package in projects_and_unclaimed_packages: + if unclaimed_package: + assert project is None + project = FakeProject(website, unclaimed_package) + out.append(project) + return out diff --git a/scss/pages/npm.scss b/scss/pages/npm.scss index 7584deeebd..0cf6eab89c 100644 --- a/scss/pages/npm.scss +++ b/scss/pages/npm.scss @@ -46,6 +46,10 @@ } } + form { + margin-top: 30px; + } + textarea { height: 192px; width: 100%; diff --git a/tests/ttw/test_package_discovery.py b/tests/ttw/test_package_discovery.py index c198b59891..46d581d696 100644 --- a/tests/ttw/test_package_discovery.py +++ b/tests/ttw/test_package_discovery.py @@ -16,8 +16,28 @@ def test_auth_gets_discovery_page_by_default(self): self.visit('/on/npm/') assert self.css('.instructions').text == 'Paste a package.json to find packages:' - def est_pasting_a_package_json_works(self): - self.make_participant('alice') + def test_pasting_a_package_json_works(self): + self.make_package(name='amperize', description='Amperize!') + glob = self.make_package(name='glob', description='Glob!', emails=['bob@example.com']) + self.make_package(name='netjet', description='Netjet!', emails=['cat@example.com']) + self.claim_package(self.make_participant('alice'), 'amperize') + self.claim_package(self.make_participant('bob'), 'glob') + + glob.team.update(name='Glub') + self.sign_in('alice') self.visit('/on/npm/') - import pdb; pdb.set_trace() + self.css('textarea').fill('''\ + + { "dependencies": { "glob": "..." , "amperize": "..."} + , "optionalDependencies": {"netjet": "...", "falafel": "..."} + } + + ''') + self.css('form button').click() + + names = [x.text for x in self.css('.listing-name')] + assert names == ['glob \u2192 Glub', 'amperize', 'netjet', 'falafel'] + + enabled = [not x.has_class('disabled') for x in self.css('td.item')] + assert enabled == [True, True, True, False] diff --git a/www/on/npm/index.html.spt b/www/on/npm/index.html.spt index 1bdb0063e7..ea7d0f2896 100644 --- a/www/on/npm/index.html.spt +++ b/www/on/npm/index.html.spt @@ -1,4 +1,10 @@ -from gratipay.utils import icons, tabs +import json +from collections import OrderedDict + +from aspen import Response +from gratipay.utils import icons, tabs, listings, ghost + +GROUPS = ('dependencies', 'devDependencies', 'optionalDependencies') [---] banner = manager = page_id = "npm" suppress_sidebar = True @@ -14,6 +20,53 @@ i18ned_flows = {'pay': _('Pay'), 'receive' : _('Receive')} tab_html = lambda key, tab: '{}'.format(i18ned_flows[key]) tabs = tabs.make(tab_html, 'flow', flow, 'pay', 'receive') +deps, devdeps, optdeps = [], [], [] +if request.method == 'POST': + try: + package_json = json.loads(request.body['package.json'], object_pairs_hook=OrderedDict) + except: + raise Response(400) + + # We want a single list of (source, name, package, project, is_ready) + # tuples. Source is 'dependencies', 'devDependencies', or + # 'optionalDependencies'. We want name in the tuple in case there is no + # package. Package and team should be None if we don't have one. is_ready + # is: + # + # True claimed package + # False unclaimed package + # None unknown package + + groups = [package_json.get(g, OrderedDict()) for g in GROUPS] + assert all(d.__class__ is OrderedDict for d in groups) + + # Load package and project (team) objects (in a single database call). + alldeps = set() + [map(alldeps.add, d) for d in groups] + known = website.db.all(''' + SELECT p.name + , p.*::packages package + , t.*::teams project + FROM packages p + LEFT JOIN teams_to_packages t2p + ON p.id = t2p.package_id + LEFT JOIN teams t + ON t2p.team_id = t.id + WHERE package_manager='npm' + AND p.name = ANY(%s) + ''', (list(alldeps),)) + known = {n:(p,t) for n,p,t in known} + + # Replace ranges with tuples. + for source, deps in zip(GROUPS, groups): + for name in deps: + package, project = known.get(name, (None, None)) + is_ready = bool(project) if package else None + deps[name] = (source, name, package, project, is_ready) + + # Flatten into a single list, martellily: https://stackoverflow.com/a/952952 + discovered = [rec for group in groups for rec in group.values()] + [---] {% from "templates/nav-tabs.html" import nav_tabs with context %} {% extends "templates/base.html" %} @@ -50,176 +103,58 @@ tabs = tabs.make(tab_html, 'flow', flow, 'pay', 'receive') {{ nav_tabs(tabs) }} {% if flow == None %} -
- {{ _('Paste a package.json to find packages:') }} -
+ {% if discovered %} +
+ {% if project %}
+
+ {% else %}
+
+ {% endif %}
+
-
+ {% if project and project.name != package.name %}
+
+ {{ package.name }} → {{ project.name }}
+
+ {% elif project %}
+ {{ package.name }}
+ {% elif package %}
+ {{ package.name }}
+ {% else %}
+ {{ name }}
+ {% endif %}
-
+
+ {{ i }}
+ ·
+ {{ group }}
+ ·
+ {{ package.description or _('unknown package') }}
+
+ |
+
diff --git a/www/search.spt b/www/search.spt index 24d825ea27..4e9e86f027 100644 --- a/www/search.spt +++ b/www/search.spt @@ -1,4 +1,4 @@ -from gratipay.utils import markdown, truncate, icons +from gratipay.utils import markdown, truncate, icons, listings from gratipay.utils.i18n import LANGUAGES_2, SEARCH_CONFS, strip_accents from markupsafe import Markup [---] @@ -17,26 +17,6 @@ def get_processed_excerpt(participant): statement = truncate(statement, 128, '') return process_excerpt(statement) -class FakeProject(object): - - def __init__(self, package): - self.package = package - self.name = package.name - self.url_path = '/on/{}/{}/'.format(package.package_manager, package.name) - - def get_image_url(self, size): - assert size in ('large', 'small'), size - return website.asset('package-default-{}.png'.format(size)) - -def with_unclaimed_packages_wrapped(projects_and_unclaimed_packages): - out = [] - for project, unclaimed_package in projects_and_unclaimed_packages: - if unclaimed_package: - assert project is None - project = FakeProject(unclaimed_package) - out.append(project) - return out - # Can't factor this out because of translations? i18ned_statuses = { "approved": _("Approved") , "unreviewed" : _("Unreviewed") @@ -213,7 +193,7 @@ zip = zip {% set usernames = zip(results.get('usernames', []), empty_excerpts()) %} {% set statements = results.get('statements') %} {% set emails = zip(results.get('emails', []), empty_excerpts()) %} - {% set projects = with_unclaimed_packages_wrapped(results.get('projects', [])) %} + {% set projects = listings.with_unclaimed_packages_wrapped(website, results.get('projects', [])) %} {% if projects %}