├── .dockerignore ├── .drone.yml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGES ├── Dockerfile ├── LICENSE ├── README.md ├── VERSION ├── Vagrantfile ├── bin └── bump_version.py ├── data └── projects │ └── .gitkeep ├── docker-compose.yml ├── docker ├── compile-assets.sh ├── entry ├── nginx │ ├── nginx.conf │ ├── proxy_portia_server.conf │ └── proxy_slyd.conf ├── portia.conf ├── provision.sh ├── qt_install.qs ├── restore-mtime.sh └── run-tests.sh ├── docs ├── Makefile ├── _static │ ├── getting-started-1.png │ ├── portia-add-start-pages.png │ ├── portia-annotation-creation.png │ ├── portia-annotation.png │ ├── portia-change-selection-mode.png │ ├── portia-configuring-crawling.png │ ├── portia-extracted-items.png │ ├── portia-extractors.png │ ├── portia-follow-patterns.png │ ├── portia-goto-extractors.png │ ├── portia-icon-add-repeat.png │ ├── portia-icon-add.png │ ├── portia-icon-pointer.png │ ├── portia-icon-sub.png │ ├── portia-icon-toggle-links.png │ ├── portia-icon-wand.png │ ├── portia-item-editor.png │ ├── portia-landing-page.png │ ├── portia-main-page.png │ ├── portia-multi-last.png │ ├── portia-multi-preview.png │ ├── portia-new-project.png │ ├── portia-new-spider.png │ ├── portia-sample-multiple-fields.png │ ├── portia-spider-link-crawling.png │ ├── portia-spider-properties.png │ └── portia-start-urls.png ├── conf.py ├── examples.rst ├── faq.rst ├── getting-started.rst ├── index.rst ├── installation.rst ├── items.rst ├── make.bat ├── projects.rst ├── samples.rst └── spiders.rst ├── portia_server ├── db_repo │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── __init__.py │ │ └── slyd_to_django.sql │ ├── models.py │ └── repo.py ├── manage.py ├── portia_api │ ├── __init__.py │ ├── apps.py │ ├── errors.py │ ├── jsonapi │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── parsers.py │ │ ├── registry.py │ │ ├── relationships.py │ │ ├── renderers.py │ │ ├── response.py │ │ ├── serializers.py │ │ └── utils.py │ ├── resources │ │ ├── __init__.py │ │ ├── annotations.py │ │ ├── extractors.py │ │ ├── fields.py │ │ ├── items.py │ │ ├── models.py │ │ ├── projects.py │ │ ├── response.py │ │ ├── route.py │ │ ├── samples.py │ │ ├── schemas.py │ │ ├── serializers.py │ │ └── spiders.py │ ├── routers.py │ ├── tests │ │ ├── __init__.py │ │ └── test_routes.py │ ├── urls.py │ └── utils │ │ ├── __init__.py │ │ ├── annotations.py │ │ ├── copy.py │ │ ├── deploy │ │ ├── base.py │ │ ├── package.py │ │ ├── scrapinghub.py │ │ └── scrapyd.py │ │ ├── download.py │ │ ├── extract.py │ │ ├── projects.py │ │ └── spiders.py ├── portia_orm │ ├── __init__.py │ ├── apps.py │ ├── base.py │ ├── collection.py │ ├── datastore.py │ ├── decorators.py │ ├── deletion.py │ ├── exceptions.py │ ├── fields.py │ ├── middleware.py │ ├── models.py │ ├── registry.py │ ├── relationships.py │ ├── serializers.py │ ├── snapshots.py │ ├── tests │ │ ├── __init__.py │ │ ├── models.py │ │ ├── test_basic.py │ │ ├── test_collection.py │ │ ├── test_model.py │ │ ├── test_relationship.py │ │ └── utils.py │ ├── utils.py │ └── validators.py ├── portia_server │ ├── __init__.py │ ├── backends.py │ ├── models.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── requirements.txt └── storage │ ├── __init__.py │ ├── apps.py │ ├── backends.py │ ├── jsondiff.py │ ├── projecttemplates.py │ └── repoman.py ├── portiaui ├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .gitignore ├── .jshintrc ├── .watchmanconfig ├── app │ ├── adapters │ │ ├── application.js │ │ └── project.js │ ├── app.js │ ├── components │ │ ├── .gitkeep │ │ ├── add-start-url-button.js │ │ ├── animation-container.js │ │ ├── annotation-options.js │ │ ├── browser-iframe.js │ │ ├── browser-url-blocked.js │ │ ├── browser-url-failing.js │ │ ├── browser-view-port.js │ │ ├── buffered-input.js │ │ ├── colored-badge.js │ │ ├── colored-span.js │ │ ├── combo-box.js │ │ ├── create-project-button.js │ │ ├── create-spider-button.js │ │ ├── data-structure-annotations.js │ │ ├── data-structure-listing.js │ │ ├── dropdown-delete.js │ │ ├── dropdown-divider.js │ │ ├── dropdown-header.js │ │ ├── dropdown-item.js │ │ ├── dropdown-menu.js │ │ ├── dropdown-widget.js │ │ ├── edit-sample-button.js │ │ ├── element-overlay.js │ │ ├── element-rect-overlay.js │ │ ├── extracted-item-table.js │ │ ├── extracted-items-group.js │ │ ├── extracted-items-json-panel.js │ │ ├── extracted-items-json-value.js │ │ ├── extracted-items-json.js │ │ ├── extracted-items-panel.js │ │ ├── extracted-items-status.js │ │ ├── extracted-items-tab.js │ │ ├── extractor-options.js │ │ ├── feed-url-options.js │ │ ├── field-options.js │ │ ├── fragment-options.js │ │ ├── generated-url-options.js │ │ ├── help-icon.js │ │ ├── icon-button.js │ │ ├── indentation-spacer.js │ │ ├── input-with-clear.js │ │ ├── inspector-panel.js │ │ ├── link-crawling-options.js │ │ ├── list-item-add-annotation-menu.js │ │ ├── list-item-annotation-field.js │ │ ├── list-item-badge.js │ │ ├── list-item-combo.js │ │ ├── list-item-editable.js │ │ ├── list-item-field-type.js │ │ ├── list-item-icon-menu.js │ │ ├── list-item-icon.js │ │ ├── list-item-item-schema.js │ │ ├── list-item-link-crawling.js │ │ ├── list-item-relation-manager.js │ │ ├── list-item-selectable.js │ │ ├── list-item-text.js │ │ ├── notification-container.js │ │ ├── notification-message.js │ │ ├── page-actions-editor.js │ │ ├── project-list.js │ │ ├── project-listing.js │ │ ├── project-structure-listing.js │ │ ├── project-structure-spider-feed-url.js │ │ ├── project-structure-spider-generated-url.js │ │ ├── project-structure-spider-url.js │ │ ├── regex-pattern-list.js │ │ ├── reorder-handler.js │ │ ├── save-status.js │ │ ├── schema-structure-listing.js │ │ ├── scrapinghub-links.js │ │ ├── select-box.js │ │ ├── show-links-button.js │ │ ├── show-links-legend.js │ │ ├── sliding-main.js │ │ ├── spider-indentation.js │ │ ├── spider-message.js │ │ ├── spider-options.js │ │ ├── spider-row.js │ │ ├── spider-structure-listing.js │ │ ├── start-url-options.js │ │ ├── tool-group.js │ │ ├── tool-panel.js │ │ ├── tool-tab.js │ │ ├── tooltip-container.js │ │ ├── tooltip-icon.js │ │ ├── tree-list-item-row.js │ │ ├── tree-list-item.js │ │ ├── tree-list.js │ │ └── url-bar.js │ ├── controllers │ │ ├── .gitkeep │ │ └── projects │ │ │ ├── project.js │ │ │ └── project │ │ │ ├── conflicts.js │ │ │ ├── conflicts │ │ │ └── conflict.js │ │ │ ├── schema │ │ │ └── field │ │ │ │ └── options.js │ │ │ ├── spider.js │ │ │ └── spider │ │ │ ├── link-options.js │ │ │ ├── options.js │ │ │ └── sample │ │ │ ├── data.js │ │ │ └── data │ │ │ └── annotation │ │ │ └── options.js │ ├── helpers │ │ ├── .gitkeep │ │ ├── array-get.js │ │ ├── attribute-annotation.js │ │ ├── chain-actions.js │ │ ├── guid.js │ │ ├── includes.js │ │ ├── indexed-object.js │ │ ├── is-empty-object.js │ │ ├── is-object-or-array.js │ │ └── is-object.js │ ├── index.html │ ├── initializers │ │ └── ui-state.js │ ├── instance-initializers │ │ └── error-handler.js │ ├── mixins │ │ ├── options-route.js │ │ └── save-spider-mixin.js │ ├── models │ │ ├── .gitkeep │ │ ├── annotation.js │ │ ├── base-annotation.js │ │ ├── base.js │ │ ├── extractor.js │ │ ├── field.js │ │ ├── item.js │ │ ├── project.js │ │ ├── sample.js │ │ ├── schema.js │ │ ├── spider.js │ │ └── start-url.js │ ├── resolver.js │ ├── router.js │ ├── routes │ │ ├── .gitkeep │ │ ├── application.js │ │ ├── browsers.js │ │ ├── index.js │ │ ├── projects.js │ │ └── projects │ │ │ ├── project.js │ │ │ └── project │ │ │ ├── compatibility.js │ │ │ ├── conflicts.js │ │ │ ├── conflicts │ │ │ └── conflict.js │ │ │ ├── schema.js │ │ │ ├── schema │ │ │ ├── field.js │ │ │ └── field │ │ │ │ └── options.js │ │ │ ├── spider.js │ │ │ └── spider │ │ │ ├── link-options.js │ │ │ ├── options.js │ │ │ ├── sample.js │ │ │ ├── sample │ │ │ ├── data.js │ │ │ ├── data │ │ │ │ ├── annotation.js │ │ │ │ ├── annotation │ │ │ │ │ └── options.js │ │ │ │ └── item.js │ │ │ └── index.js │ │ │ ├── start-url.js │ │ │ └── start-url │ │ │ └── options.js │ ├── serializers │ │ └── application.js │ ├── services │ │ ├── annotation-structure.js │ │ ├── browser.js │ │ ├── capabilities.js │ │ ├── changes.js │ │ ├── clock.js │ │ ├── dispatcher.js │ │ ├── extracted-items.js │ │ ├── notification-manager.js │ │ ├── overlays.js │ │ ├── position-monitor.js │ │ ├── saving-notification.js │ │ ├── selector-matcher.js │ │ ├── store.js │ │ ├── ui-state.js │ │ └── web-socket.js │ ├── storages │ │ ├── cookies.js │ │ ├── page-loads.js │ │ ├── ui-state-collapsed-panels.js │ │ └── ui-state-selected-tools.js │ ├── styles │ │ ├── _animations.scss │ │ ├── _bootstrap_overrides.scss │ │ ├── _icons.scss │ │ ├── _lib_config.scss │ │ ├── _variables.scss │ │ ├── app.scss │ │ ├── components │ │ │ ├── animation-container.scss │ │ │ ├── browser-iframe.scss │ │ │ ├── browser-view-port.scss │ │ │ ├── combo-box.scss │ │ │ ├── conflicts.scss │ │ │ ├── dropdown-delete.scss │ │ │ ├── dropdown-menu.scss │ │ │ ├── dropdown-widget.scss │ │ │ ├── extracted-item-table.scss │ │ │ ├── extracted-items-json-panel.scss │ │ │ ├── extractor-options.scss │ │ │ ├── fragment-options.scss │ │ │ ├── help-icon.scss │ │ │ ├── icon-button.scss │ │ │ ├── indentation-spacer.scss │ │ │ ├── input-with-clear.scss │ │ │ ├── inspector-panel.scss │ │ │ ├── list-item-badge.scss │ │ │ ├── list-item-combo.scss │ │ │ ├── list-item-editable.scss │ │ │ ├── list-item-icon.scss │ │ │ ├── list-item-selectable.scss │ │ │ ├── list-item-text.scss │ │ │ ├── notifications.scss │ │ │ ├── page-actions.scss │ │ │ ├── project-structure-spider-generation-url.scss │ │ │ ├── regex-pattern-list.scss │ │ │ ├── save-status.scss │ │ │ ├── select-box.scss │ │ │ ├── show-links-legend.scss │ │ │ ├── side-bar.scss │ │ │ ├── sliding-main.scss │ │ │ ├── start-url-options.scss │ │ │ ├── tool-group.scss │ │ │ ├── tool-panel.scss │ │ │ ├── tooltip-container.scss │ │ │ ├── top-bar.scss │ │ │ ├── tree-list.scss │ │ │ └── url-bar.scss │ │ ├── document.scss │ │ ├── droplet.scss │ │ ├── generic.scss │ │ ├── layout │ │ │ ├── _clickable.scss │ │ │ ├── _forms.scss │ │ │ └── _full-page-content.scss │ │ └── templates │ │ │ ├── application.scss │ │ │ ├── browsers.scss │ │ │ └── projects.scss │ ├── templates │ │ ├── application.hbs │ │ ├── branding.hbs │ │ ├── browsers.hbs │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── add-start-url-button.hbs │ │ │ ├── animation-container.hbs │ │ │ ├── annotation-options.hbs │ │ │ ├── browser-iframe.hbs │ │ │ ├── browser-list.hbs │ │ │ ├── browser-url-blocked.hbs │ │ │ ├── browser-url-failing.hbs │ │ │ ├── browser-view-port.hbs │ │ │ ├── buffered-input.hbs │ │ │ ├── colored-badge.hbs │ │ │ ├── colored-span.hbs │ │ │ ├── combo-box.hbs │ │ │ ├── create-project-button.hbs │ │ │ ├── create-spider-button.hbs │ │ │ ├── data-structure-annotations.hbs │ │ │ ├── data-structure-listing.hbs │ │ │ ├── dropdown-delete.hbs │ │ │ ├── dropdown-divider.hbs │ │ │ ├── dropdown-header.hbs │ │ │ ├── dropdown-item.hbs │ │ │ ├── dropdown-menu.hbs │ │ │ ├── dropdown-widget.hbs │ │ │ ├── edit-sample-button.hbs │ │ │ ├── element-overlay.hbs │ │ │ ├── element-rect-overlay.hbs │ │ │ ├── extracted-item-table.hbs │ │ │ ├── extracted-items-group.hbs │ │ │ ├── extracted-items-json-panel.hbs │ │ │ ├── extracted-items-json-value.hbs │ │ │ ├── extracted-items-json.hbs │ │ │ ├── extracted-items-panel.hbs │ │ │ ├── extracted-items-status.hbs │ │ │ ├── extracted-items-tab.hbs │ │ │ ├── extractor-options.hbs │ │ │ ├── feed-url-options.hbs │ │ │ ├── field-options.hbs │ │ │ ├── fragment-options.hbs │ │ │ ├── generated-url-options.hbs │ │ │ ├── help-icon.hbs │ │ │ ├── icon-button.hbs │ │ │ ├── input-with-clear.hbs │ │ │ ├── inspector-panel.hbs │ │ │ ├── json-file-compare.hbs │ │ │ ├── link-crawling-options.hbs │ │ │ ├── list-item-add-annotation-menu.hbs │ │ │ ├── list-item-annotation-field.hbs │ │ │ ├── list-item-badge.hbs │ │ │ ├── list-item-combo.hbs │ │ │ ├── list-item-editable.hbs │ │ │ ├── list-item-field-type.hbs │ │ │ ├── list-item-icon-menu.hbs │ │ │ ├── list-item-icon.hbs │ │ │ ├── list-item-item-schema.hbs │ │ │ ├── list-item-link-crawling.hbs │ │ │ ├── list-item-relation-manager.hbs │ │ │ ├── list-item-selectable.hbs │ │ │ ├── list-item-text.hbs │ │ │ ├── notification-container.hbs │ │ │ ├── notification-message.hbs │ │ │ ├── page-actions-editor.hbs │ │ │ ├── project-list.hbs │ │ │ ├── project-listing.hbs │ │ │ ├── project-structure-listing.hbs │ │ │ ├── project-structure-spider-feed-url.hbs │ │ │ ├── project-structure-spider-generated-url.hbs │ │ │ ├── project-structure-spider-url.hbs │ │ │ ├── regex-pattern-list.hbs │ │ │ ├── save-status.hbs │ │ │ ├── schema-structure-listing.hbs │ │ │ ├── scrapinghub-links.hbs │ │ │ ├── select-box.hbs │ │ │ ├── show-links-button.hbs │ │ │ ├── show-links-legend.hbs │ │ │ ├── sliding-main.hbs │ │ │ ├── spider-indentation.hbs │ │ │ ├── spider-message.hbs │ │ │ ├── spider-options.hbs │ │ │ ├── spider-row.hbs │ │ │ ├── spider-structure-listing.hbs │ │ │ ├── start-url-options.hbs │ │ │ ├── tool-group.hbs │ │ │ ├── tool-panel.hbs │ │ │ ├── tool-tab.hbs │ │ │ ├── tooltip-container.hbs │ │ │ ├── tooltip-icon.hbs │ │ │ ├── tree-list-item-row.hbs │ │ │ ├── tree-list-item.hbs │ │ │ ├── tree-list.hbs │ │ │ └── url-bar.hbs │ │ ├── options-panels.hbs │ │ ├── projects.hbs │ │ ├── projects │ │ │ ├── project.hbs │ │ │ └── project │ │ │ │ ├── conflicts │ │ │ │ ├── file-selector.hbs │ │ │ │ ├── help.hbs │ │ │ │ ├── resolver.hbs │ │ │ │ └── topbar.hbs │ │ │ │ ├── schema.hbs │ │ │ │ ├── schema │ │ │ │ ├── field.hbs │ │ │ │ ├── field │ │ │ │ │ └── options.hbs │ │ │ │ └── structure.hbs │ │ │ │ ├── spider.hbs │ │ │ │ ├── spider │ │ │ │ ├── link-options.hbs │ │ │ │ ├── options.hbs │ │ │ │ ├── overlays.hbs │ │ │ │ ├── sample.hbs │ │ │ │ ├── sample │ │ │ │ │ ├── annotation │ │ │ │ │ │ └── selection.hbs │ │ │ │ │ ├── data.hbs │ │ │ │ │ ├── data │ │ │ │ │ │ ├── annotation.hbs │ │ │ │ │ │ ├── annotation │ │ │ │ │ │ │ └── options.hbs │ │ │ │ │ │ ├── item.hbs │ │ │ │ │ │ ├── overlays.hbs │ │ │ │ │ │ ├── structure.hbs │ │ │ │ │ │ ├── toolbar.hbs │ │ │ │ │ │ └── tools.hbs │ │ │ │ │ ├── item.hbs │ │ │ │ │ ├── structure.hbs │ │ │ │ │ └── toolbar.hbs │ │ │ │ ├── start-url │ │ │ │ │ └── options.hbs │ │ │ │ ├── structure.hbs │ │ │ │ ├── toolbar.hbs │ │ │ │ └── tools.hbs │ │ │ │ ├── structure.hbs │ │ │ │ └── toolbar.hbs │ │ └── tool-panels.hbs │ ├── transforms │ │ ├── array.js │ │ ├── json.js │ │ └── start-url.js │ ├── utils │ │ ├── attrs.js │ │ ├── browser-features.js │ │ ├── colors.js │ │ ├── computed.js │ │ ├── ensure-promise.js │ │ ├── interaction-event.js │ │ ├── promises.js │ │ ├── selectors.js │ │ ├── start-urls.js │ │ ├── tree-mirror-delegate.js │ │ ├── types.js │ │ └── utils.js │ ├── validations │ │ ├── fixed-fragment.js │ │ ├── list-fragment.js │ │ └── range-fragment.js │ └── validators │ │ ├── range.js │ │ └── whitespace.js ├── bower.json ├── config │ ├── deprecation-workflow.js │ ├── environment-development.js │ ├── environment-production.js │ ├── environment-test.js │ └── environment.js ├── ember-cli-build.js ├── package-lock.json ├── package.json ├── public │ ├── assets │ │ └── images │ │ │ ├── chrome-logo.jpg │ │ │ ├── firefox-logo.png │ │ │ └── portia-logo.svg │ ├── crossdomain.xml │ ├── empty-frame.html │ ├── frames-not-supported.html │ └── robots.txt ├── testem.js ├── tests │ ├── .jshintrc │ ├── helpers │ │ ├── destroy-app.js │ │ ├── module-for-acceptance.js │ │ ├── resolver.js │ │ └── start-app.js │ ├── index.html │ ├── test-helper.js │ └── unit │ │ ├── .gitkeep │ │ ├── models │ │ └── start-url-test.js │ │ ├── utils │ │ ├── selectors-test.js │ │ └── start-urls-test.js │ │ └── validators │ │ ├── range-test.js │ │ └── whitespace-test.js └── vendor │ ├── .gitkeep │ ├── modernizr.js │ ├── mutation-summary.js │ └── tree-mirror.js ├── slybot ├── .gitignore ├── CHANGES ├── MANIFEST.in ├── Makefile.buildbot ├── README.rst ├── bin │ ├── makedeb │ ├── portiacrawl │ └── slybot ├── debian │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── pyversions │ └── rules ├── docs │ ├── Makefile │ ├── conf.py │ ├── index.rst │ ├── make.bat │ ├── project.rst │ └── spiderlets.rst ├── requirements-clustering.txt ├── requirements-test.txt ├── requirements.txt ├── scrapy.cfg ├── setup.py ├── slybot │ ├── __init__.py │ ├── baseurl.py │ ├── closespider.py │ ├── clustering.py │ ├── dupefilter.py │ ├── exporter.py │ ├── extractors.py │ ├── fieldtypes │ │ ├── __init__.py │ │ ├── date.py │ │ ├── images.py │ │ ├── number.py │ │ ├── point.py │ │ ├── price.py │ │ ├── text.py │ │ └── url.py │ ├── generic_form.py │ ├── item.py │ ├── linkextractor │ │ ├── __init__.py │ │ ├── base.py │ │ ├── ecsv.py │ │ ├── html.py │ │ ├── pagination.py │ │ ├── regex.py │ │ └── xml.py │ ├── meta.py │ ├── pageactions.py │ ├── plugins │ │ ├── __init__.py │ │ ├── scrapely_annotations │ │ │ ├── __init__.py │ │ │ ├── annotations.py │ │ │ ├── builder.py │ │ │ ├── exceptions.py │ │ │ ├── extraction │ │ │ │ ├── __init__.py │ │ │ │ ├── container_extractors.py │ │ │ │ ├── extractors.py │ │ │ │ ├── pageparsing.py │ │ │ │ ├── region_extractors.py │ │ │ │ └── utils.py │ │ │ ├── migration.py │ │ │ ├── processors.py │ │ │ └── utils.py │ │ └── selectors │ │ │ └── __init__.py │ ├── settings.py │ ├── spider.py │ ├── spiderlets.py │ ├── spidermanager.py │ ├── splash.py │ ├── starturls │ │ ├── __init__.py │ │ ├── feed_generator.py │ │ ├── fragment_generator.py │ │ ├── generated_url.py │ │ └── generator.py │ ├── tests │ │ ├── __init__.py │ │ ├── data │ │ │ ├── SampleProject │ │ │ │ ├── extractors.json │ │ │ │ ├── items.json │ │ │ │ ├── project.json │ │ │ │ └── spiders │ │ │ │ │ ├── allowed_domains.json │ │ │ │ │ ├── any_allowed_domains.json │ │ │ │ │ ├── books.toscrape.com.json │ │ │ │ │ ├── books.toscrape.com │ │ │ │ │ ├── 3617-44af-a2f0.json │ │ │ │ │ ├── 3617-44af-a2f0 │ │ │ │ │ │ └── original_body.html │ │ │ │ │ ├── 3652-4fa1-a912.json │ │ │ │ │ ├── 4583-41b4-9edb.json │ │ │ │ │ └── 4583-41b4-9edb │ │ │ │ │ │ └── original_body.html │ │ │ │ │ ├── books.toscrape.com_1.json │ │ │ │ │ ├── cargurus.json │ │ │ │ │ ├── ebay.json │ │ │ │ │ ├── ebay2.json │ │ │ │ │ ├── ebay3.json │ │ │ │ │ ├── ebay4.json │ │ │ │ │ ├── example.com.json │ │ │ │ │ ├── example2.com.json │ │ │ │ │ ├── example3.com.json │ │ │ │ │ ├── example4.com.json │ │ │ │ │ ├── networkhealth.com.json │ │ │ │ │ ├── networkhealth.com │ │ │ │ │ ├── networkhealthtemplate.json │ │ │ │ │ └── networkhealthtemplate │ │ │ │ │ │ ├── annotated_body.html │ │ │ │ │ │ └── original_body.html │ │ │ │ │ ├── pinterest.com.json │ │ │ │ │ ├── seedsofchange.com.json │ │ │ │ │ ├── seedsofchange.json │ │ │ │ │ ├── seedsofchange2.json │ │ │ │ │ └── sitemaps.json │ │ │ ├── atom_sample.xml │ │ │ ├── ebay_advanced_search.html │ │ │ ├── pinterest.html │ │ │ ├── rss_sample.xml │ │ │ ├── sitemap_sample.xml │ │ │ ├── templates │ │ │ │ ├── 411_list.json │ │ │ │ ├── autoevolution.html │ │ │ │ ├── autoevolution.json │ │ │ │ ├── autoevolution2.json │ │ │ │ ├── cars.com.json │ │ │ │ ├── cars.com_nested.json │ │ │ │ ├── cs-cart.json │ │ │ │ ├── daft_ie.html │ │ │ │ ├── daft_list.json │ │ │ │ ├── firmen.wko.at.html │ │ │ │ ├── firmen.wko.at.json │ │ │ │ ├── hn.html │ │ │ │ ├── patchofland.html │ │ │ │ ├── so_annotations.json │ │ │ │ ├── stack_overflow.html │ │ │ │ ├── stips.co.il.html │ │ │ │ ├── stips.co.il.json │ │ │ │ └── xceed.json │ │ │ └── test_params.txt │ │ ├── test_baseurl.py │ │ ├── test_dropmeta.py │ │ ├── test_dupefilter.py │ │ ├── test_extraction_speed.py │ │ ├── test_extractors.py │ │ ├── test_fieldtypes.py │ │ ├── test_fragment_generator.py │ │ ├── test_generic_form.py │ │ ├── test_linkextractors.py │ │ ├── test_migration.py │ │ ├── test_multiple_item_extraction.py │ │ ├── test_page_actions.py │ │ ├── test_schema_validation.py │ │ ├── test_selectors.py │ │ ├── test_spider.py │ │ ├── test_starturls.py │ │ ├── test_starturls_generator.py │ │ └── utils.py │ ├── utils.py │ └── validation │ │ ├── __init__.py │ │ ├── schema.py │ │ └── schemas.json └── tox.ini ├── slyd ├── .gitignore ├── .jshintrc ├── README.md ├── bin │ ├── init_mysql_db │ ├── sh2sly │ └── slyd ├── requirements.txt ├── setup.py ├── slybot ├── slyd │ ├── __init__.py │ ├── authmanager.py │ ├── dummyauth.py │ ├── errors.py │ ├── gitstorage │ │ ├── __init__.py │ │ ├── jsondiff.py │ │ ├── projects.py │ │ └── projectspec.py │ ├── html_utils.py │ ├── projects.py │ ├── projectspec.py │ ├── resource.py │ ├── server.py │ ├── settings │ │ ├── __init__.py │ │ └── base.py │ ├── specmanager.py │ ├── splash │ │ ├── __init__.py │ │ ├── commands.py │ │ ├── cookies.py │ │ ├── css_utils.py │ │ ├── ferry.py │ │ ├── proxy.py │ │ ├── qtutils.py │ │ └── utils.py │ └── tap.py └── twisted │ └── plugins │ └── slyd_plugin.py └── splash_utils ├── compile_slybot.sh ├── filters └── easylist.txt ├── perform_actions.js ├── waitAsync.js └── z_inject_this.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vagrant 3 | docs 4 | */node_modules 5 | */bower_components 6 | */tests 7 | */tmp 8 | */db.sqlite3 9 | */.tox 10 | */.pyc 11 | */__pycache__ 12 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | image: scrapinghub 2 | 3 | script: 4 | - echo "Portia is at:"`git show -s --pretty=%d HEAD` 5 | - git restore-mtime 6 | - shopt -s extglob 7 | - nvm install 10.16.0 8 | - nvm use 10.16.0 9 | - sudo mkdir -p ~/.npm ~/.node-gyp ~/.cache 10 | - sudo chown -R ubuntu ~/.npm ~/.node-gyp ~/.cache 11 | - npm install -g bower ember-cli@2.6.3 --cache-min 999999 12 | - docker/compile-assets.sh 13 | - build_docker_image 14 | - publish_to_dockerhub 15 | 16 | cache: 17 | - /home/ubuntu/.npm 18 | - /home/ubuntu/.node-gyp 19 | - /home/ubuntu/.cache 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.css] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [*.html] 29 | indent_style = space 30 | indent_size = 2 31 | 32 | [*.{diff,md}] 33 | trim_trailing_whitespace = false 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh eol=lf 2 | *.bat eol=crlf 3 | *.js text 4 | *.py text 5 | *.css text 6 | *.hbs text 7 | *.json text 8 | *.html text 9 | *.xml text 10 | *.yml text 11 | *.txt text 12 | *.rst text 13 | *.md text 14 | *.cfg text 15 | *.conf text 16 | Makefile* text 17 | 18 | *.png binary 19 | *.swf binary 20 | *.ttf binary 21 | *.woff binary 22 | *.woff2 binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python compiled files 2 | *__pycache__/* 3 | *.pyc 4 | 5 | # Vagrant files 6 | .vagrant/ 7 | /.idea/ 8 | 9 | # Python build files 10 | *.egg-info 11 | slybot/dist 12 | slybot/build 13 | slyd/slyd/dist 14 | slyd/slyd/build 15 | 16 | # npm files 17 | node_modules/* 18 | slyd/bower_components/* 19 | slyd/tmp/* 20 | npm-debug.log 21 | slybot/slybot/splash-script-combined.js 22 | 23 | # Local Settings 24 | slyd/slyd/local_settings.py 25 | slybot/slybot/local_slybot_settings.py 26 | 27 | # Testing 28 | slybot/.tox 29 | 30 | # Docs build directory 31 | docs/_build 32 | 33 | # Development Databases 34 | *.sqlite* 35 | 36 | # Default Portia data directory 37 | slyd/slyd/data 38 | /data/ 39 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.8 2 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # vim:ft=ruby 2 | 3 | Vagrant.configure("2") do |config| 4 | config.vm.box = "ubuntu/trusty64" 5 | config.vm.host_name = "portia" 6 | config.vm.provision :shell, :path => 'docker/provision.sh', :args => [ 7 | "install_deps", "install_splash", "install_python_deps", "configure_nginx", "configure_initctl", "migrate_django_db", "start_portia" 8 | ] 9 | config.vm.network "private_network", ip: "33.33.33.10" 10 | config.vm.network "forwarded_port", guest: 9001, host: 9001 11 | config.vm.provider "virtualbox" do |v| 12 | v.memory = 2048 13 | v.cpus = 2 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /data/projects/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/data/projects/.gitkeep -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | command: /app/docker/entry start-dev 6 | volumes: 7 | - ./data/projects:/app/data/projects:rw 8 | - ./portiaui/dist:/app/portiaui/dist 9 | - ./slyd:/app/slyd 10 | - ./portia_server:/app/portia_server 11 | - ./slybot:/app/slybot 12 | ports: 13 | - 9001:9001 14 | restart: always 15 | -------------------------------------------------------------------------------- /docker/compile-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd portiaui 3 | npm install 4 | npm run build 5 | -------------------------------------------------------------------------------- /docker/entry: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | action=$1 4 | shift 5 | 6 | _run() { 7 | service nginx start 8 | _set_env 9 | echo $PYTHONPATH 10 | /app/slyd/bin/slyd -p 9002 -r /app/portiaui/dist & 11 | /app/portia_server/manage.py runserver 12 | } 13 | 14 | _set_env() { 15 | path='/app/portia_server:/app/slyd:/app/slybot' 16 | export PYTHONPATH="$path" 17 | } 18 | 19 | if [ -z "$action" ]; then 20 | _run 21 | else 22 | case $action in 23 | start-dev|start-prod) 24 | _run 25 | ;; 26 | start-webshell) 27 | _run_webshell "$@" 28 | ;; 29 | *) 30 | exec $action "$@" 31 | ;; 32 | esac 33 | fi -------------------------------------------------------------------------------- /docker/nginx/proxy_portia_server.conf: -------------------------------------------------------------------------------- 1 | proxy_pass http://127.0.0.1:8000; 2 | proxy_redirect off; 3 | proxy_set_header Host $http_host; 4 | proxy_set_header X-Real-IP $remote_addr; 5 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 6 | proxy_set_header X-Forwarded-Host $server_name; 7 | -------------------------------------------------------------------------------- /docker/nginx/proxy_slyd.conf: -------------------------------------------------------------------------------- 1 | proxy_pass http://127.0.0.1:9002; 2 | proxy_redirect off; 3 | proxy_set_header Host $host:9002; 4 | proxy_set_header X-Real-IP $remote_addr; 5 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 6 | proxy_set_header X-Forwarded-Host $server_name; 7 | -------------------------------------------------------------------------------- /docker/portia.conf: -------------------------------------------------------------------------------- 1 | description "portia server" 2 | start on vagrant-mounted or filesystem 3 | stop on runlevel [!2345] 4 | 5 | script 6 | export PYTHONPATH='/vagrant/portia_server:/vagrant/slyd:/vagrant/slybot' 7 | /vagrant/slyd/bin/slyd -p 9002 -r /vagrant/portiaui/dist & 8 | /vagrant/portia_server/manage.py runserver 9 | end script 10 | respawn 11 | -------------------------------------------------------------------------------- /docker/restore-mtime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | commit=$(git rev-list -n 1 HEAD requirements.txt) 3 | mtime=$(git show --pretty=format:%ai --abbrev-commit $commit |head -n1) 4 | touch -d "$mtime" requirements.txt 5 | -------------------------------------------------------------------------------- /docker/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PYTHONPATH=`pwd`/slybot:`pwd`/slyd 4 | pip install tox 5 | 6 | cd /app/slyd 7 | python2.7 tests/testserver/server.py 2>&1 | grep -v 'HTTP/1.1" 200' & 8 | sleep 3 9 | 10 | cd /app/slybot 11 | tox 12 | cd /app/portia_server 13 | ./manage.py test portia_orm.tests 14 | ./manage.py test portia_api.tests 15 | -------------------------------------------------------------------------------- /docs/_static/getting-started-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/getting-started-1.png -------------------------------------------------------------------------------- /docs/_static/portia-add-start-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-add-start-pages.png -------------------------------------------------------------------------------- /docs/_static/portia-annotation-creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-annotation-creation.png -------------------------------------------------------------------------------- /docs/_static/portia-annotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-annotation.png -------------------------------------------------------------------------------- /docs/_static/portia-change-selection-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-change-selection-mode.png -------------------------------------------------------------------------------- /docs/_static/portia-configuring-crawling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-configuring-crawling.png -------------------------------------------------------------------------------- /docs/_static/portia-extracted-items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-extracted-items.png -------------------------------------------------------------------------------- /docs/_static/portia-extractors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-extractors.png -------------------------------------------------------------------------------- /docs/_static/portia-follow-patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-follow-patterns.png -------------------------------------------------------------------------------- /docs/_static/portia-goto-extractors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-goto-extractors.png -------------------------------------------------------------------------------- /docs/_static/portia-icon-add-repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-icon-add-repeat.png -------------------------------------------------------------------------------- /docs/_static/portia-icon-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-icon-add.png -------------------------------------------------------------------------------- /docs/_static/portia-icon-pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-icon-pointer.png -------------------------------------------------------------------------------- /docs/_static/portia-icon-sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-icon-sub.png -------------------------------------------------------------------------------- /docs/_static/portia-icon-toggle-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-icon-toggle-links.png -------------------------------------------------------------------------------- /docs/_static/portia-icon-wand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-icon-wand.png -------------------------------------------------------------------------------- /docs/_static/portia-item-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-item-editor.png -------------------------------------------------------------------------------- /docs/_static/portia-landing-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-landing-page.png -------------------------------------------------------------------------------- /docs/_static/portia-main-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-main-page.png -------------------------------------------------------------------------------- /docs/_static/portia-multi-last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-multi-last.png -------------------------------------------------------------------------------- /docs/_static/portia-multi-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-multi-preview.png -------------------------------------------------------------------------------- /docs/_static/portia-new-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-new-project.png -------------------------------------------------------------------------------- /docs/_static/portia-new-spider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-new-spider.png -------------------------------------------------------------------------------- /docs/_static/portia-sample-multiple-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-sample-multiple-fields.png -------------------------------------------------------------------------------- /docs/_static/portia-spider-link-crawling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-spider-link-crawling.png -------------------------------------------------------------------------------- /docs/_static/portia-spider-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-spider-properties.png -------------------------------------------------------------------------------- /docs/_static/portia-start-urls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/docs/_static/portia-start-urls.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Portia's documentation! 2 | ================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | installation 10 | getting-started 11 | examples 12 | projects 13 | spiders 14 | samples 15 | items 16 | faq 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /portia_server/db_repo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/db_repo/__init__.py -------------------------------------------------------------------------------- /portia_server/db_repo/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class DbRepoConfig(AppConfig): 7 | name = 'db_repo' 8 | -------------------------------------------------------------------------------- /portia_server/db_repo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/db_repo/migrations/__init__.py -------------------------------------------------------------------------------- /portia_server/db_repo/migrations/slyd_to_django.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `objs` DROP PRIMARY KEY, 2 | ADD COLUMN `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | ADD CONSTRAINT `objs_oid_feda89ac_uniq` UNIQUE (`oid`, `repo`), 4 | CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, 5 | ALTER COLUMN `oid` DROP DEFAULT; 6 | DROP INDEX `type` ON `objs`; 7 | DROP INDEX `size` ON `objs`; 8 | CREATE INDEX `objs_599dcce2` ON `objs` (`type`); 9 | CREATE INDEX `objs_f7bd60b7` ON `objs` (`size`); 10 | 11 | ALTER TABLE `refs` DROP PRIMARY KEY, 12 | ADD COLUMN `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, 13 | ADD CONSTRAINT `refs_ref_4a751775_uniq` UNIQUE (`ref`, `repo`), 14 | CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, 15 | ALTER COLUMN `ref` DROP DEFAULT; 16 | DROP INDEX `value` ON `refs`; 17 | CREATE INDEX `refs_2063c160` ON `refs` (`value`); -------------------------------------------------------------------------------- /portia_server/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portia_server.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /portia_server/portia_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_api/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_api/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class PortiaApiConfig(AppConfig): 7 | name = 'portia_api' 8 | -------------------------------------------------------------------------------- /portia_server/portia_api/jsonapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .response import JSONResponse 2 | -------------------------------------------------------------------------------- /portia_server/portia_api/jsonapi/parsers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from rest_framework.parsers import JSONParser 4 | 5 | 6 | class JSONApiParser(JSONParser): 7 | media_type = 'application/vnd.api+json' 8 | -------------------------------------------------------------------------------- /portia_server/portia_api/jsonapi/registry.py: -------------------------------------------------------------------------------- 1 | from portia_orm.exceptions import ImproperlyConfigured 2 | 3 | 4 | __all__ = [ 5 | 'schema', 6 | ] 7 | 8 | schemas = {} 9 | 10 | 11 | def get_schema(schema_type): 12 | try: 13 | return schemas[schema_type] 14 | except KeyError: 15 | raise ImproperlyConfigured( 16 | u"No schema for type '{}' exists".format(schema_type)) 17 | -------------------------------------------------------------------------------- /portia_server/portia_api/jsonapi/response.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from rest_framework.renderers import JSONRenderer 3 | 4 | 5 | class JSONResponse(HttpResponse): 6 | """ 7 | An HttpResponse that renders its content into JSON. 8 | """ 9 | def __init__(self, data, **kwargs): 10 | content = JSONRenderer().render(data) 11 | kwargs['content_type'] = 'application/json' 12 | super(JSONResponse, self).__init__(content, **kwargs) 13 | -------------------------------------------------------------------------------- /portia_server/portia_api/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_api/resources/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_api/resources/extractors.py: -------------------------------------------------------------------------------- 1 | from .projects import BaseProjectModelRoute 2 | from portia_orm.models import Extractor 3 | 4 | 5 | class ExtractorRoute(BaseProjectModelRoute): 6 | lookup_url_kwarg = 'extractor_id' 7 | default_model = Extractor 8 | 9 | def get_instance(self): 10 | return self.get_collection()[self.kwargs.get('extractor_id')] 11 | 12 | def get_collection(self): 13 | return self.project.extractors 14 | -------------------------------------------------------------------------------- /portia_server/portia_api/routers.py: -------------------------------------------------------------------------------- 1 | from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter 2 | 3 | __all__ = [ 4 | 'Router', 5 | 'NestedRouter', 6 | ] 7 | 8 | 9 | class Router(SimpleRouter): 10 | def __init__(self, trailing_slash=False): 11 | super(Router, self).__init__(trailing_slash) 12 | 13 | def get_lookup_regex(self, viewset, lookup_prefix=''): 14 | return super(Router, self).get_lookup_regex(viewset, '') 15 | 16 | 17 | class NestedRouter(NestedSimpleRouter, Router): 18 | def __init__(self, parent_router, parent_prefix, trailing_slash=False, 19 | *args, **kwargs): 20 | super(NestedRouter, self).__init__( 21 | parent_router, parent_prefix, trailing_slash, *args, **kwargs) 22 | -------------------------------------------------------------------------------- /portia_server/portia_api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_api/tests/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_api/tests/test_routes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from rest_framework.test import APIRequestFactory 4 | 5 | from portia_api.resources.route import JsonApiRoute 6 | 7 | 8 | class TestRoute(unittest.TestCase): 9 | def test_route_representation(self): 10 | factory = APIRequestFactory() 11 | request = factory.get('/projects/') 12 | route = JsonApiRoute(request=request) 13 | self.assertEqual(str(route), 'GET /projects/') 14 | self.assertEqual(repr(route), 'Route(GET /projects/)') 15 | -------------------------------------------------------------------------------- /portia_server/portia_api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_api/utils/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_api/utils/annotations.py: -------------------------------------------------------------------------------- 1 | 2 | DEFAULTS = { 3 | 'accept': 'url', 4 | 'align': 'number', 5 | 'code': 'url', 6 | 'codebase': 'url', 7 | 'coords': 'geopoint', 8 | 'data': 'url', 9 | 'datetime': 'date', 10 | 'download': 'url', 11 | 'high': 'number', 12 | 'href': 'url', 13 | 'icon': 'image', 14 | 'low': 'number', 15 | 'max': 'number', 16 | 'media': 'href', 17 | 'min': 'number', 18 | 'optimum': 'number', 19 | 'rel': 'href', 20 | 'rows': 'number', 21 | 'src': 'image', 22 | 'target': 'url', 23 | } 24 | 25 | 26 | def choose_field_type(annotation): 27 | attribute = annotation.attribute 28 | if attribute == 'content': 29 | return 'text' 30 | return DEFAULTS.get(attribute, 'text') 31 | -------------------------------------------------------------------------------- /portia_server/portia_api/utils/deploy/base.py: -------------------------------------------------------------------------------- 1 | from portia_api.utils.download import ProjectArchiver 2 | 3 | 4 | class BaseDeploy(object): 5 | def __init__(self, project): 6 | self.project = project 7 | self.storage = project.storage 8 | self.config = self._get_config() 9 | self.config.version = self.project.version 10 | 11 | def build_archive(self): 12 | return ProjectArchiver(self.storage, project=self.project).archive( 13 | egg_info=True) 14 | 15 | def _get_config(self): 16 | raise NotImplementedError 17 | 18 | def deploy(self, target=None): 19 | raise NotImplementedError 20 | 21 | def schedule(self, spider, args=None, settings=None, target=None): 22 | raise NotImplementedError 23 | -------------------------------------------------------------------------------- /portia_server/portia_api/utils/projects.py: -------------------------------------------------------------------------------- 1 | def unique_name(base_name, disallow=(), initial_suffix=''): 2 | disallow = set(disallow) 3 | suffix = initial_suffix 4 | while True: 5 | name = u'{}{}'.format(base_name, suffix) 6 | if name not in disallow: 7 | break 8 | try: 9 | suffix += 1 10 | except TypeError: 11 | suffix = 1 12 | return name 13 | -------------------------------------------------------------------------------- /portia_server/portia_orm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_orm/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_orm/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class PortiaOrmConfig(AppConfig): 7 | name = 'portia_orm' 8 | -------------------------------------------------------------------------------- /portia_server/portia_orm/decorators.py: -------------------------------------------------------------------------------- 1 | from marshmallow.decorators import (validates, validates_schema, 2 | pre_dump, post_dump, pre_load, post_load) 3 | 4 | __all__ = [ 5 | 'validates', 6 | 'validates_schema', 7 | 'pre_dump', 8 | 'post_dump', 9 | 'pre_load', 10 | 'post_load', 11 | ] 12 | -------------------------------------------------------------------------------- /portia_server/portia_orm/exceptions.py: -------------------------------------------------------------------------------- 1 | from marshmallow.exceptions import ValidationError 2 | 3 | __all__ = [ 4 | 'ImproperlyConfigured', 5 | 'ValidationError', 6 | ] 7 | 8 | 9 | class ImproperlyConfigured(Exception): 10 | pass 11 | 12 | 13 | class PathResolutionError(Exception): 14 | pass 15 | 16 | 17 | class ProtectedError(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /portia_server/portia_orm/middleware.py: -------------------------------------------------------------------------------- 1 | from .datastore import data_store_context 2 | 3 | 4 | class ORMDataStoreMiddleware(object): 5 | def __init__(self, get_response=None): 6 | self.get_response = get_response 7 | 8 | def __call__(self, request): 9 | with data_store_context(): 10 | return self.get_response(request) 11 | -------------------------------------------------------------------------------- /portia_server/portia_orm/registry.py: -------------------------------------------------------------------------------- 1 | from six import itervalues 2 | 3 | from .exceptions import ImproperlyConfigured 4 | 5 | 6 | __all__ = [ 7 | 'get_model', 8 | 'get_polymorphic_model', 9 | ] 10 | 11 | models = {} 12 | 13 | 14 | def get_model(model_name): 15 | try: 16 | return models[model_name] 17 | except KeyError: 18 | raise ImproperlyConfigured( 19 | u"No model named '{}' exists".format(model_name)) 20 | 21 | 22 | def get_polymorphic_model(data): 23 | for model in itervalues(models): 24 | polymorphic = model.opts.polymorphic 25 | if polymorphic: 26 | polymorphic_key = polymorphic 27 | if isinstance(polymorphic_key, bool): 28 | polymorphic_key = 'type' 29 | if data.get(polymorphic_key) == model.__name__: 30 | return model 31 | raise ImproperlyConfigured( 32 | u"No model found for data: {!r}".format(data)) 33 | -------------------------------------------------------------------------------- /portia_server/portia_orm/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_orm/tests/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_orm/validators.py: -------------------------------------------------------------------------------- 1 | from marshmallow.validate import (ContainsOnly, Range, Regexp, Predicate, 2 | NoneOf, OneOf) 3 | 4 | __all__ = [ 5 | 'ContainsOnly', 6 | 'Range', 7 | 'Regexp', 8 | 'Predicate', 9 | 'NoneOf', 10 | 'OneOf' 11 | ] 12 | -------------------------------------------------------------------------------- /portia_server/portia_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portia_server/portia_server/__init__.py -------------------------------------------------------------------------------- /portia_server/portia_server/backends.py: -------------------------------------------------------------------------------- 1 | from .models import LocalUser 2 | 3 | 4 | class LocalAuthentication(object): 5 | def authenticate(self, request, **kwargs): 6 | return LocalUser(**kwargs), None 7 | 8 | def get_user(self, user_id): 9 | # fall through and let the middleware add the user again 10 | return None 11 | -------------------------------------------------------------------------------- /portia_server/portia_server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | 3 | from . import views 4 | from portia_api import urls 5 | 6 | urlpatterns = [ 7 | url(r'^api/', include((urls, 'api'), namespace='api')), 8 | url(r'^server_capabilities$', views.capabilities), 9 | ] 10 | -------------------------------------------------------------------------------- /portia_server/portia_server/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from portia_api.jsonapi import JSONResponse 3 | 4 | 5 | def capabilities(request): 6 | capabilities = { 7 | 'custom': settings.CUSTOM, 8 | 'username': request.user.username, 9 | 'capabilities': settings.CAPABILITIES, 10 | } 11 | return JSONResponse(capabilities) 12 | -------------------------------------------------------------------------------- /portia_server/portia_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for portia_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portia_server.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /portia_server/requirements.txt: -------------------------------------------------------------------------------- 1 | crochet==1.9.0 2 | django>=1.11.21 3 | django-cache-machine==1.0.0 4 | djangorestframework==3.7.7 5 | dj-database-url==0.5.0 6 | drf-nested-routers==0.11.1 7 | dulwich==0.18.6 8 | marshmallow==2.8.0 9 | marshmallow_jsonapi==0.10.0 10 | mysqlclient==1.3.12 11 | requests>=2.20.0 12 | toposort==1.5 13 | whitenoise==3.3.1 14 | portia2code==0.0.17 15 | shub==2.7.0 16 | -------------------------------------------------------------------------------- /portia_server/storage/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.module_loading import import_string 3 | 4 | __all__ = [ 5 | 'get_storage_class', 6 | 'create_project_storage', 7 | ] 8 | 9 | storage_class = None 10 | 11 | 12 | def get_storage_class(): 13 | global storage_class 14 | if storage_class is None and settings.PORTIA_STORAGE_BACKEND: 15 | storage_class = import_string(settings.PORTIA_STORAGE_BACKEND) 16 | storage_class.setup() 17 | return storage_class 18 | 19 | 20 | def create_project_storage(project_id, author=None, branch=None): 21 | storage_class = get_storage_class() 22 | return storage_class(project_id, author=author) 23 | -------------------------------------------------------------------------------- /portia_server/storage/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class StorageConfig(AppConfig): 5 | name = 'storage' 6 | -------------------------------------------------------------------------------- /portiaui/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /portiaui/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [*.hbs] 21 | indent_style = space 22 | indent_size = 4 23 | 24 | [*.css] 25 | indent_style = space 26 | indent_size = 4 27 | 28 | [*.html] 29 | indent_style = space 30 | indent_size = 4 31 | 32 | [*.{diff,md}] 33 | trim_trailing_whitespace = false 34 | -------------------------------------------------------------------------------- /portiaui/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | "disableAnalytics": true 3 | } 4 | -------------------------------------------------------------------------------- /portiaui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /portiaui/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "$", 6 | "cookie", 7 | "moment", 8 | "URI", 9 | "TreeMirror", 10 | "-Promise", 11 | "Raven", 12 | "Modernizr" 13 | ], 14 | "browser": true, 15 | "boss": true, 16 | "curly": true, 17 | "debug": false, 18 | "devel": true, 19 | "eqeqeq": true, 20 | "evil": true, 21 | "forin": false, 22 | "immed": false, 23 | "indent": 4, 24 | "laxbreak": false, 25 | "maxlen": 100, 26 | "newcap": true, 27 | "noarg": true, 28 | "noempty": false, 29 | "nonew": false, 30 | "nomen": false, 31 | "onevar": false, 32 | "plusplus": false, 33 | "regexp": false, 34 | "undef": true, 35 | "sub": true, 36 | "strict": false, 37 | "white": false, 38 | "eqnull": true, 39 | "esnext": true, 40 | "unused": true 41 | } 42 | -------------------------------------------------------------------------------- /portiaui/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /portiaui/app/adapters/project.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlTemplate: '{+host}/api/projects{/id}', 5 | findRecordUrlTemplate: '{+host}/api/projects{/id}', 6 | createRecordUrlTemplate: '{+host}/api/projects', 7 | shouldReloadRecord() { return true; } 8 | }); 9 | -------------------------------------------------------------------------------- /portiaui/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver, 14 | 15 | customEvents: { 16 | transitionend: 'transitionEnd' 17 | } 18 | }); 19 | 20 | loadInitializers(App, config.modulePrefix); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /portiaui/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/components/.gitkeep -------------------------------------------------------------------------------- /portiaui/app/components/browser-url-blocked.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'span', 5 | }); 6 | -------------------------------------------------------------------------------- /portiaui/app/components/browser-url-failing.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { inject: { service } } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: 'span', 6 | browser: service(), 7 | 8 | actions: { 9 | reloadPage() { 10 | this.get('browser').reload(); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /portiaui/app/components/colored-badge.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '', 5 | 6 | color: null, 7 | value: 0, 8 | 9 | badgeStyle: Ember.computed('color.main', function() { 10 | var color = this.get('color.main'); 11 | return Ember.String.htmlSafe(color ? `background-color: ${color};` : ''); 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /portiaui/app/components/colored-span.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: 'span', 6 | attributeBindings: ['colorStyle:style'], 7 | colorStyle: computed('color.main', function() { 8 | var color = this.get('color.main'); 9 | return Ember.String.htmlSafe(color ? `color: ${color};` : ''); 10 | }) 11 | }); 12 | -------------------------------------------------------------------------------- /portiaui/app/components/create-project-button.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed, inject: { service } } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | dispatcher: service(), 6 | capabilities: service(), 7 | tagName: '', 8 | 9 | canCreateProjects: computed.readOnly('capabilities.capabilities.create_projects'), 10 | projectName: null, 11 | 12 | actions: { 13 | addProject() { 14 | this.get('dispatcher').addProject(this.get('projectName'), /* redirect = */true); 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /portiaui/app/components/create-spider-button.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import {computedCanAddSpider} from '../services/dispatcher'; 3 | 4 | export default Ember.Component.extend({ 5 | browser: Ember.inject.service(), 6 | dispatcher: Ember.inject.service(), 7 | 8 | tagName: '', 9 | 10 | project: null, 11 | 12 | canAddSpider: computedCanAddSpider(), 13 | 14 | actions: { 15 | addSpider() { 16 | this.get('dispatcher').addSpider(this.get('project'), /* redirect = */true); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /portiaui/app/components/data-structure-listing.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | dispatcher: Ember.inject.service(), 5 | 6 | tagName: '', 7 | 8 | annotationColors: [], 9 | 10 | actions: { 11 | addItem(sample) { 12 | this.get('dispatcher').addItem(sample, /* redirect = */true); 13 | }, 14 | 15 | removeItem(item) { 16 | this.get('dispatcher').removeItem(item); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /portiaui/app/components/dropdown-delete.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: 'li', 6 | classNames: ['dropdown-delete'], 7 | classNameBindings: ['isConfirmed'], 8 | isConfirmed: false, 9 | 10 | notConfirmed: computed.not('isConfirmed'), 11 | 12 | actions: { 13 | onDelete() { 14 | if (this.get('notConfirmed')) { 15 | this.set('isConfirmed', true); 16 | } else { 17 | this.get('onDelete')(); 18 | } 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /portiaui/app/components/dropdown-divider.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'li', 5 | classNames: ['divider'], 6 | attributeBindings: ['role'], 7 | role: 'separator' 8 | }); 9 | -------------------------------------------------------------------------------- /portiaui/app/components/dropdown-header.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'li', 5 | classNames: ['dropdown-header'] 6 | }); 7 | -------------------------------------------------------------------------------- /portiaui/app/components/extracted-item-table.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | 4 | export default Ember.Component.extend({ 5 | tagName: 'table', 6 | classNames: ['extracted-item-table'] 7 | }); 8 | -------------------------------------------------------------------------------- /portiaui/app/components/extracted-items-group.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '' 5 | }); 6 | -------------------------------------------------------------------------------- /portiaui/app/components/extracted-items-json-panel.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | extractedItems: Ember.inject.service(), 5 | 6 | tagName: '' 7 | }); 8 | -------------------------------------------------------------------------------- /portiaui/app/components/extracted-items-panel.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { inject: { service }, computed } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: '', 6 | 7 | extractedItems: service(), 8 | 9 | isExtracting: computed.readOnly('extractedItems.isExtracting'), 10 | failedMsg: computed.readOnly('extractedItems.failedExtractionMsg'), 11 | failedExtraction: computed.readOnly('extractedItems.failedExtraction') 12 | }); 13 | -------------------------------------------------------------------------------- /portiaui/app/components/extracted-items-tab.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { inject: { service }, computed } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | extractedItems: service(), 6 | 7 | numItems: computed.readOnly('extractedItems.items.length'), 8 | isExtracting: computed.alias('extractedItems.isExtracting') 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/components/feed-url-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { cleanUrl } from '../utils/utils'; 3 | 4 | export default Ember.Component.extend({ 5 | feedLink: 'http://files.scrapinghub.com/portia/urls.txt', 6 | 7 | didRender() { 8 | this._super(...arguments); 9 | this.$('.focus-control').focus(); 10 | }, 11 | 12 | actions: { 13 | saveFeedUrl() { 14 | const url = cleanUrl(this.get('startUrl.url')); 15 | this.set('startUrl.url', url); 16 | this.get('saveSpider').perform(); 17 | } 18 | } 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /portiaui/app/components/field-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '', 5 | 6 | field: null, 7 | 8 | actions: { 9 | save() { 10 | this.get('field').save(); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /portiaui/app/components/help-icon.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '', 5 | 6 | tooltipClasses: null, 7 | tooltipContainer: 'body', 8 | placement: 'right', 9 | icon: 'help', 10 | classes: 'help-icon', 11 | }); 12 | -------------------------------------------------------------------------------- /portiaui/app/components/indentation-spacer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['indentation-spacer'], 5 | classNameBindings: ['isSmall'], 6 | isSmall: false 7 | }); 8 | -------------------------------------------------------------------------------- /portiaui/app/components/input-with-clear.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['input-group', 'input-with-clear'], 5 | 6 | type: 'text', 7 | value: '', 8 | 9 | actions: { 10 | clear() { 11 | this.set('value', ''); 12 | this.get('clear')(); 13 | }, 14 | 15 | keyUp() { 16 | this.update(this.get('value')); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /portiaui/app/components/link-crawling-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import SaveSpiderMixin from '../mixins/save-spider-mixin'; 3 | 4 | export default Ember.Component.extend(SaveSpiderMixin,{ 5 | tagName: '', 6 | 7 | spider: null, 8 | 9 | actions: { 10 | save() { 11 | this.saveSpider(); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-add-annotation-menu.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from '../config/environment'; 3 | 4 | 5 | export default Ember.Component.extend({ 6 | dispatcher: Ember.inject.service(), 7 | 8 | tagName: '', 9 | 10 | item: null, 11 | 12 | allowNesting: config.APP.allow_nesting, 13 | 14 | actions: { 15 | addAnnotation() { 16 | const item = this.get('item'); 17 | this.get('dispatcher').addAnnotation(item, undefined, undefined, /* redirect = */true); 18 | }, 19 | 20 | addNestedItem() { 21 | const item = this.get('item'); 22 | this.get('dispatcher').addNestedItem(item, /* redirect = */true); 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-badge.js: -------------------------------------------------------------------------------- 1 | import ColoredBadge from './colored-badge'; 2 | 3 | export default ColoredBadge.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-combo.js: -------------------------------------------------------------------------------- 1 | import ListItemSelectable from './list-item-selectable'; 2 | 3 | export default ListItemSelectable.extend({ 4 | classNames: ['list-item-combo'], 5 | 6 | autoSelect: false 7 | }); 8 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-editable.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['list-item-editable'], 5 | classNameBindings: ['editing'], 6 | 7 | editing: false, 8 | onChange: null, 9 | validate: null, 10 | spellcheck: true, 11 | value: null, 12 | 13 | click() { 14 | if (this.get('editing')) { 15 | return false; 16 | } 17 | }, 18 | 19 | actions: { 20 | startEditing() { 21 | this.set('editing', true); 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-field-type.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { FIELD_TYPES } from '../models/field'; 3 | import ensurePromise from '../utils/ensure-promise'; 4 | 5 | export default Ember.Component.extend({ 6 | tagName: '', 7 | 8 | field: null, 9 | 10 | types: FIELD_TYPES, 11 | 12 | actions: { 13 | saveField() { 14 | const field = this.get('field'); 15 | ensurePromise(field).then(field => { 16 | if (!!field) { 17 | field.save(); 18 | } 19 | }); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-icon-menu.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '', 5 | 6 | icon: null, 7 | 8 | actions: { 9 | clickIcon() { 10 | const action = this.get('onClick'); 11 | if (action) { action(); } 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-icon.js: -------------------------------------------------------------------------------- 1 | import IconButton from './icon-button'; 2 | 3 | export default IconButton.extend({ 4 | classNames: ['list-item-icon'], 5 | 6 | beforeClick() { 7 | const action = this.get('onClick'); 8 | if (action) { action(); } 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-item-schema.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | dispatcher: Ember.inject.service(), 5 | 6 | tagName: '', 7 | 8 | item: null, 9 | selecting: false, 10 | 11 | actions: { 12 | addSchema(name) { 13 | const item = this.get('item'); 14 | const project = item.get('schema.project'); 15 | this.get('dispatcher').addNamedSchema( 16 | project, name, /* redirect = */false).then((schema) => { 17 | item.set('schema', schema); 18 | item.save(); 19 | }); 20 | }, 21 | 22 | changeSchema() { 23 | const item = this.get('item'); 24 | item.get('schema').then(() => { 25 | item.set('schema', item.get('schema')); // Used to trigger updates 26 | item.save(); 27 | }); 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-selectable.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['list-item-selectable'], 5 | classNameBindings: ['selecting'], 6 | 7 | change: null, 8 | choices: [], 9 | buttonClass: null, 10 | menuAlign: 'left', 11 | menuClass: null, 12 | menuContainer: null, 13 | 14 | selecting: false, 15 | value: null, 16 | 17 | actions: { 18 | startSelecting() { 19 | this.set('selecting', true); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/components/list-item-text.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'span', 5 | classNames: ['list-item-text'] 6 | }); 7 | -------------------------------------------------------------------------------- /portiaui/app/components/project-structure-spider-feed-url.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed } = Ember; 3 | import { cleanUrl } from '../utils/utils'; 4 | 5 | export default Ember.Component.extend({ 6 | dispatcher: Ember.inject.service(), 7 | 8 | tagName: '', 9 | 10 | url: computed.alias('startUrl.url'), 11 | isEditing: computed.equal('url', ''), 12 | 13 | viewUrl: computed('url', { 14 | get() { 15 | return this.get('url'); 16 | }, 17 | set(key, value) { 18 | this.saveStartUrl(value); 19 | } 20 | }), 21 | 22 | saveStartUrl(url) { 23 | this.set('startUrl.url', cleanUrl(url)); 24 | this.get('spider').save(); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /portiaui/app/components/project-structure-spider-generated-url.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: '', 6 | 7 | fragments: computed.alias('startUrl.fragments'), 8 | url: computed('startUrl.url', 'fragments.@each.type', 'fragments.@each.value', function() { 9 | return this.get('startUrl').show(); 10 | }) 11 | }); 12 | -------------------------------------------------------------------------------- /portiaui/app/components/reorder-handler.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | attributeBindings: ['draggable', 'style'], 5 | draggable: true, 6 | tagName: 'i', 7 | classNames: 'fa fa-icon fa-arrows reorder-handler', 8 | dragStart: function(event) { 9 | var dataTransfer = event.originalEvent.dataTransfer; 10 | dataTransfer.effectAllowed = "move"; 11 | dataTransfer.setData('text/plain', ""); 12 | var dragElement = this.$().parentsUntil('.reorderable-list').eq(-1); 13 | dataTransfer.addElement(dragElement[0]); 14 | dragElement.addClass('dragging').one("dragend", function(){ 15 | dragElement.removeClass('dragging'); 16 | }); 17 | }, 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /portiaui/app/components/scrapinghub-links.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '', 5 | }); 6 | -------------------------------------------------------------------------------- /portiaui/app/components/show-links-button.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed, inject: { service } } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | browser: service(), 6 | 7 | disableLinks: computed.readOnly('browser.invalidUrl'), 8 | spider: null, 9 | 10 | actions: { 11 | toggleShowLinks() { 12 | const spider = this.get('spider'); 13 | spider.toggleProperty('showLinks'); 14 | spider.save(); 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /portiaui/app/components/show-links-legend.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { NAMED_COLORS } from '../utils/colors'; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: '', 6 | 7 | colors: NAMED_COLORS, 8 | followedLinks: 0, 9 | jsLinks: 0, 10 | ignoredLinks: 0 11 | }); 12 | -------------------------------------------------------------------------------- /portiaui/app/components/sliding-main.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | uiState: Ember.inject.service(), 5 | 6 | tagName: 'main', 7 | classNameBindings: ['slideRight'], 8 | 9 | slideRight: Ember.computed.bool('uiState.slideMain') 10 | }); 11 | -------------------------------------------------------------------------------- /portiaui/app/components/spider-indentation.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ tagName: '' }); 4 | -------------------------------------------------------------------------------- /portiaui/app/components/spider-message.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed } = Ember; 3 | 4 | export default Ember.Component.extend({ 5 | api: Ember.inject.service(), 6 | notificationManager: Ember.inject.service(), 7 | hasSpider: computed.bool('currentSpider'), 8 | 9 | actions: { 10 | runSpider(spider) { 11 | this.get('api').post('schedule', { 12 | model: spider, 13 | jsonData: {data: {type: 'spiders', id: spider.id}} 14 | }).then(() => { 15 | this.get('notificationManager').showNotification( 16 | 'Your spider has been scheduled successfully'); 17 | }, data => { 18 | let error = data.errors[0]; 19 | if (error.status > 499) { 20 | throw data; 21 | } 22 | this.get('notificationManager').showNotification(error.title, error.detail); 23 | }); 24 | } 25 | } 26 | }); -------------------------------------------------------------------------------- /portiaui/app/components/spider-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import SaveSpiderMixin from '../mixins/save-spider-mixin'; 3 | 4 | export default Ember.Component.extend(SaveSpiderMixin, { 5 | tagName: '', 6 | 7 | spider: null, 8 | 9 | actions: { 10 | save() { 11 | this.saveSpider(); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /portiaui/app/components/start-url-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed } = Ember; 3 | import { task, timeout } from 'ember-concurrency'; 4 | 5 | const SPIDER_DEBOUNCE = 1000; 6 | 7 | export default Ember.Component.extend({ 8 | startUrl: computed('spider.startUrls.[]', 'startUrlId', function() { 9 | return this.get('spider').get('startUrls').objectAt(this.get('startUrlId')); 10 | }), 11 | 12 | title: computed.alias('startUrl.optionsTitle'), 13 | 14 | saveSpider: task(function * () { 15 | yield timeout(SPIDER_DEBOUNCE); 16 | this.get('spider').save(); 17 | }).restartable() 18 | }); 19 | -------------------------------------------------------------------------------- /portiaui/app/components/tool-panel.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import {computedPropertiesEqual} from '../utils/computed'; 3 | 4 | export default Ember.Component.extend({ 5 | classNames: ['tool-panel'], 6 | classNameBindings: ['active::hide'], 7 | 8 | active: computedPropertiesEqual('toolId', 'group.selected') 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/components/tool-tab.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import {computedPropertiesEqual} from '../utils/computed'; 3 | 4 | export default Ember.Component.extend({ 5 | tagName: 'li', 6 | classNameBindings: ['active'], 7 | 8 | active: computedPropertiesEqual('toolId', 'group.selected'), 9 | 10 | didInsertElement() { 11 | if (!this.$().prev().length) { 12 | Ember.run.schedule('afterRender', () => { 13 | if (!this.get('group.selected')) { 14 | this.send('selectTab'); 15 | } 16 | }); 17 | } 18 | }, 19 | 20 | actions: { 21 | selectTab() { 22 | this.get('group').send('selectTab', this.get('toolId')); 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /portiaui/app/components/tooltip-icon.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: '', 5 | 6 | actions: { 7 | onClick() { 8 | const action = this.get('onClick'); 9 | if (action) { 10 | action(); 11 | } 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /portiaui/app/components/tree-list-item-row.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['tree-list-item-row'], 5 | 6 | mouseEnter() { 7 | if (this.attrs.onMouseEnter && this.attrs.onMouseEnter.call) { 8 | this.attrs.onMouseEnter(...arguments); 9 | } 10 | }, 11 | 12 | mouseLeave() { 13 | if (this.attrs.onMouseLeave && this.attrs.onMouseLeave.call) { 14 | this.attrs.onMouseLeave(...arguments); 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /portiaui/app/components/tree-list-item.js: -------------------------------------------------------------------------------- 1 | import AnimationContainer from './animation-container'; 2 | 3 | export default AnimationContainer.extend({ 4 | tagName: 'li', 5 | classNames: ['tree-list-item'], 6 | 7 | setWidth: false, 8 | isCentered: false, 9 | hasChildren: false 10 | }); 11 | -------------------------------------------------------------------------------- /portiaui/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/controllers/.gitkeep -------------------------------------------------------------------------------- /portiaui/app/controllers/projects/project.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | browser: Ember.inject.service(), 5 | 6 | queryParams: ['url', 'baseurl'], 7 | 8 | url: Ember.computed.alias('browser.url'), 9 | baseurl: Ember.computed.alias('browser.baseurl'), 10 | clickHandler: null, 11 | 12 | setClickHandler(fn) { 13 | this.clickHandler = fn; 14 | }, 15 | 16 | clearClickHandler() { 17 | this.clickHandler = null; 18 | }, 19 | 20 | actions: { 21 | viewPortClick() { 22 | if (this.clickHandler) { 23 | this.clickHandler(...arguments); 24 | } 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /portiaui/app/controllers/projects/project/conflicts.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | projectController: Ember.inject.controller('projects.project'), 5 | currentFileName: null, 6 | 7 | conflictedKeyPaths: {}, 8 | 9 | conflictedFiles: Ember.computed('model', function() { 10 | return Object.keys(this.get('model')).sort().map((name) => ({ 11 | name: name, 12 | encodedName: btoa(name), 13 | })); 14 | }), 15 | }); 16 | -------------------------------------------------------------------------------- /portiaui/app/controllers/projects/project/schema/field/options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | actions: { 5 | closeOptions() { 6 | this.send('close'); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/controllers/projects/project/spider/link-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | actions: { 5 | closeOptions() { 6 | this.send('close'); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/controllers/projects/project/spider/options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | actions: { 5 | closeOptions() { 6 | this.send('close'); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/controllers/projects/project/spider/sample/data/annotation/options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | actions: { 5 | closeOptions() { 6 | this.send('close'); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/helpers/.gitkeep -------------------------------------------------------------------------------- /portiaui/app/helpers/array-get.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Helper.extend({ 4 | compute(params/*, hash*/) { 5 | this.setProperties({ 6 | obj: params[0], 7 | index: params[1] 8 | }); 9 | 10 | return this.get('content'); 11 | }, 12 | 13 | obj: null, 14 | index: null, 15 | content: Ember.computed('obj.[]', 'index', function() { 16 | return this.get('obj').get(this.get('index')); 17 | }), 18 | 19 | contentDidChange: Ember.observer('content', function () { 20 | this.recompute(); 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/helpers/attribute-annotation.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Helper.extend({ 4 | compute([annotations, attribute]) { 5 | this.setProperties({ 6 | annotations, 7 | attribute 8 | }); 9 | 10 | return this.get('content'); 11 | }, 12 | 13 | annotations: null, 14 | attribute: null, 15 | content: Ember.computed('annotations.[]', 'attribute', function() { 16 | const attribute = this.get('attribute'); 17 | return this.getWithDefault('annotations', []).find(annotation => 18 | annotation.getWithDefault('attribute', null) === attribute) || {}; 19 | }), 20 | 21 | contentDidChange: Ember.observer('content', function () { 22 | this.recompute(); 23 | }) 24 | }); 25 | -------------------------------------------------------------------------------- /portiaui/app/helpers/chain-actions.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function chainActions(params/*, hash*/) { 4 | return function() { 5 | for (let action of params) { 6 | if (action.call) { 7 | action(); 8 | } 9 | } 10 | }; 11 | } 12 | 13 | export default Ember.Helper.helper(chainActions); 14 | -------------------------------------------------------------------------------- /portiaui/app/helpers/guid.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function guid([obj]/*, hash*/) { 4 | return Ember.guidFor(obj); 5 | } 6 | 7 | export default Ember.Helper.helper(guid); 8 | -------------------------------------------------------------------------------- /portiaui/app/helpers/includes.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function includes([list, value]) { 4 | return list && list.includes && list.includes(value); 5 | } 6 | 7 | export default Ember.Helper.helper(includes); 8 | -------------------------------------------------------------------------------- /portiaui/app/helpers/indexed-object.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function indexedObject([ param ] /*, hash*/) { 4 | let indexed = {}, i = 0; 5 | for (let key of Object.keys(param)) { 6 | indexed[key] = { 7 | index: i, 8 | value: param[key] 9 | }; 10 | i += 1; 11 | } 12 | return indexed; 13 | } 14 | 15 | export default Ember.Helper.helper(indexedObject); 16 | -------------------------------------------------------------------------------- /portiaui/app/helpers/is-empty-object.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import {isObject} from './is-object'; 3 | 4 | export function isEmptyObject(params) { 5 | return isObject(params) && !Object.keys(...params).length; 6 | } 7 | 8 | export default Ember.Helper.helper(isEmptyObject); 9 | -------------------------------------------------------------------------------- /portiaui/app/helpers/is-object-or-array.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { isArrayHelper } from 'ember-truth-helpers/helpers/is-array'; 3 | import { isObject } from './is-object'; 4 | 5 | export function isObjectOrArray(params) { 6 | return isObject(params) || isArrayHelper(params); 7 | } 8 | 9 | export default Ember.Helper.helper(isObjectOrArray); 10 | -------------------------------------------------------------------------------- /portiaui/app/helpers/is-object.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { toType } from '../utils/types'; 3 | 4 | export function isObject([object]) { 5 | return toType(object) === 'object'; 6 | } 7 | 8 | export default Ember.Helper.helper(isObject); 9 | -------------------------------------------------------------------------------- /portiaui/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Portia 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /portiaui/app/mixins/options-route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | uiState: Ember.inject.service(), 5 | 6 | activate() { 7 | this.set('uiState.slideMain', true); 8 | }, 9 | 10 | deactivate() { 11 | this.set('uiState.slideMain', false); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /portiaui/app/mixins/save-spider-mixin.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | webSocket: Ember.inject.service(), 5 | 6 | saveSpider() { 7 | let savePromise = this.get('spider').save(); 8 | savePromise.then(() => 9 | this.get('webSocket').send({ 10 | 'spider': this.get('spider.id'), 11 | 'project': this.get('spider.project.id'), 12 | '_command': 'update_spider' 13 | }) 14 | ); 15 | return savePromise; 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /portiaui/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/models/.gitkeep -------------------------------------------------------------------------------- /portiaui/app/models/base-annotation.js: -------------------------------------------------------------------------------- 1 | import { belongsTo } from 'ember-data/relationships'; 2 | import BaseModel from './base'; 3 | 4 | export default BaseModel.extend({ 5 | parent: belongsTo('item', { 6 | inverse: 'annotations' 7 | }) 8 | }); 9 | -------------------------------------------------------------------------------- /portiaui/app/models/extractor.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import BaseModel from './base'; 3 | 4 | export default BaseModel.extend({ 5 | type: DS.attr('string'), 6 | value: DS.attr('string'), 7 | project: DS.belongsTo(), 8 | annotations: DS.hasMany() 9 | }); 10 | -------------------------------------------------------------------------------- /portiaui/app/models/field.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import BaseModel from './base'; 3 | 4 | export const FIELD_TYPES = [ 5 | 'date', 'geopoint', 'image', 'number', 'price', 'raw html', 'safe html', 'text', 'url']; 6 | 7 | export default BaseModel.extend({ 8 | name: DS.attr('string'), 9 | type: DS.attr('string'), 10 | required: DS.attr('boolean'), 11 | vary: DS.attr('boolean'), 12 | schema: DS.belongsTo({ 13 | async: true 14 | }), 15 | annotations: DS.hasMany({ 16 | async: true 17 | }) 18 | }); 19 | -------------------------------------------------------------------------------- /portiaui/app/models/schema.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import BaseModel from './base'; 3 | 4 | export default BaseModel.extend({ 5 | name: DS.attr('string'), 6 | default: DS.attr('boolean'), 7 | project: DS.belongsTo(), 8 | fields: DS.hasMany(), 9 | items: DS.hasMany() 10 | }); 11 | -------------------------------------------------------------------------------- /portiaui/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /portiaui/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/routes/.gitkeep -------------------------------------------------------------------------------- /portiaui/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /portiaui/app/routes/browsers.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | let browsers = [ 4 | { 5 | name: 'Chrome', 6 | alt: 'Chrome logo', 7 | src: '/assets/images/chrome-logo.jpg', 8 | href: 'https://www.google.com/chrome/browser/desktop/' 9 | }, 10 | { 11 | name: 'Firefox', 12 | alt: 'Firefox logo', 13 | src: '/assets/images/firefox-logo.png', 14 | href: 'https://www.mozilla.org/en-US/firefox/new/' 15 | } 16 | ]; 17 | 18 | export default Ember.Route.extend({ 19 | model() { 20 | return browsers; 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import hasBrowserFeatures from '../utils/browser-features'; 3 | 4 | function identity(x) { return x; } 5 | 6 | export default Ember.Route.extend({ 7 | model() { 8 | return hasBrowserFeatures(); 9 | }, 10 | 11 | redirect(model) { 12 | let hasFeatures = model.every(identity); 13 | let nextRoute = hasFeatures ? 'projects' : 'browsers'; 14 | this.replaceWith(nextRoute); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return this.store.findAll('project'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/compatibility.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | return params; 6 | }, 7 | 8 | redirect({path}, {queryParams}) { 9 | // conflicts route has the same path 10 | if (path === 'items') { 11 | this.transitionTo('projects.project', { 12 | queryParams: queryParams 13 | }); 14 | return; 15 | } 16 | const fragments = path.split('/'); 17 | if (fragments.length === 1) { 18 | this.transitionTo('projects.project.spider', fragments[0], { 19 | queryParams: queryParams 20 | }); 21 | } else { 22 | this.transitionTo('projects.project.spider.sample', fragments[0], fragments[1], { 23 | queryParams: queryParams 24 | }); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/conflicts.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return $.post('/projects', JSON.stringify({ 6 | cmd: 'conflicts', 7 | args: [this.modelFor("projects.project").id] 8 | })); 9 | }, 10 | 11 | renderTemplate() { 12 | this.render('projects/project/conflicts/file-selector', { 13 | into: 'application', 14 | outlet: 'side-bar', 15 | }); 16 | 17 | this.render('projects/project/conflicts/help', { 18 | into: 'application', 19 | outlet: 'main', 20 | }); 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/conflicts/conflict.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | var allConflicts = this.modelFor("projects.project.conflicts"); 6 | var file = atob(params.file_path); 7 | return { 8 | file: file, 9 | contents: allConflicts[file], 10 | }; 11 | }, 12 | 13 | renderTemplate() { 14 | this.render('projects/project/conflicts/topbar', { 15 | into: 'application', 16 | outlet: 'top-bar', 17 | }); 18 | 19 | this.render('projects/project/conflicts/resolver', { 20 | into: 'application', 21 | outlet: 'main', 22 | }); 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/schema.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | return this.store.peekRecord('schema', params.schema_id); 6 | }, 7 | 8 | afterModel(model) { 9 | return model.reload(); 10 | }, 11 | 12 | renderTemplate() { 13 | this.render('projects/project/schema/structure', { 14 | into: 'projects/project/structure', 15 | outlet: 'project-structure' 16 | }); 17 | }, 18 | 19 | actions: { 20 | error: function() { 21 | this.transitionTo('projects.project', 22 | this.modelFor('projects.project')); 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/schema/field.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | return this.store.peekRecord('field', params.field_id); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/schema/field/options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import OptionsRoute from '../../../../../mixins/options-route'; 3 | 4 | export default Ember.Route.extend(OptionsRoute, { 5 | model() { 6 | return this.modelFor('projects.project.schema.field'); 7 | }, 8 | 9 | renderTemplate() { 10 | this.render('projects/project/schema/field/options', { 11 | into: 'options-panels', 12 | outlet: 'options-panels' 13 | }); 14 | }, 15 | 16 | actions: { 17 | close() { 18 | this.transitionTo('projects.project.schema.field'); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/link-options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import OptionsRoute from '../../../../mixins/options-route'; 3 | 4 | export default Ember.Route.extend(OptionsRoute, { 5 | model() { 6 | return this.modelFor('projects.project.spider'); 7 | }, 8 | 9 | renderTemplate() { 10 | this.render('projects/project/spider/link-options', { 11 | into: 'options-panels', 12 | outlet: 'options-panels' 13 | }); 14 | 15 | }, 16 | 17 | actions: { 18 | close() { 19 | this.transitionTo('projects.project.spider'); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import OptionsRoute from '../../../../mixins/options-route'; 3 | 4 | export default Ember.Route.extend(OptionsRoute, { 5 | model() { 6 | return this.modelFor('projects.project.spider'); 7 | }, 8 | 9 | renderTemplate() { 10 | this.render('projects/project/spider/options', { 11 | into: 'options-panels', 12 | outlet: 'options-panels' 13 | }); 14 | 15 | }, 16 | 17 | actions: { 18 | close() { 19 | this.transitionTo('projects.project.spider'); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/sample.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | browser: Ember.inject.service(), 5 | 6 | model(params) { 7 | return this.store.peekRecord('sample', params.sample_id); 8 | }, 9 | 10 | afterModel(model) { 11 | return model.reload().then(model => { 12 | return model; 13 | }); 14 | }, 15 | 16 | renderTemplate() { 17 | this.render('projects/project/spider/sample/structure', { 18 | into: 'projects/project/spider/structure', 19 | outlet: 'spider-structure' 20 | }); 21 | 22 | this.render('projects/project/spider/sample/toolbar', { 23 | into: 'projects/project', 24 | outlet: 'browser-toolbar' 25 | }); 26 | }, 27 | 28 | actions: { 29 | error() { 30 | this.transitionTo('projects.project.spider', 31 | this.modelFor('projects.project.spider')); 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/sample/data/annotation.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | uiState: Ember.inject.service(), 5 | selectedElement: Ember.computed.alias('uiState.viewPort.selectedElement'), 6 | selectedModel: Ember.computed.alias('uiState.viewPort.selectedModel'), 7 | 8 | model(params) { 9 | return this.store.peekRecord('annotation', params.annotation_id); 10 | }, 11 | 12 | afterModel(model) { 13 | if (this.get('selectedModel') !== model) { 14 | this.setProperties({ 15 | selectedElement: null, 16 | selectedModel: model 17 | }); 18 | } 19 | }, 20 | 21 | deactivate() { 22 | this.setProperties({ 23 | selectedElement: null, 24 | selectedModel: null 25 | }); 26 | }, 27 | 28 | actions: { 29 | error() { 30 | this.transitionTo('projects.project.spider.sample.data'); 31 | } 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/sample/data/annotation/options.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import OptionsRoute from '../../../../../../../mixins/options-route'; 3 | 4 | export default Ember.Route.extend(OptionsRoute, { 5 | model() { 6 | return this.modelFor('projects.project.spider.sample.data.annotation'); 7 | }, 8 | 9 | afterModel() { 10 | let extractorsPromise = this.modelFor('projects.project').get('extractors'); 11 | if (!extractorsPromise.get('isPending')) { 12 | extractorsPromise = extractorsPromise.reload(); 13 | } 14 | return extractorsPromise; 15 | }, 16 | 17 | renderTemplate() { 18 | this.render('projects/project/spider/sample/data/annotation/options', { 19 | into: 'options-panels', 20 | outlet: 'options-panels' 21 | }); 22 | }, 23 | 24 | actions: { 25 | close() { 26 | this.transitionTo('projects.project.spider.sample.data.annotation'); 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/sample/data/item.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | return this.store.peekRecord('item', params.item_id); 6 | }, 7 | 8 | actions: { 9 | error() { 10 | this.transitionTo('projects.project.spider.sample.data'); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/sample/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | redirect(model, {queryParams}) { 5 | this.transitionTo('projects.project.spider.sample.data', { 6 | /* The queryParams in the transition object have been processed and keys with empty 7 | values have been removed. If we use the same object for the new transition the 8 | unspecified values will keep their current values. This means we can't automatically 9 | pass through query parameters that have intentionally been emptied. */ 10 | queryParams: Ember.assign({ 11 | url: null, 12 | baseurl: null 13 | }, queryParams) 14 | }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /portiaui/app/routes/projects/project/spider/start-url.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | const spider = this.modelFor('projects.project.spider'); 6 | return spider.get('startUrls').objectAt(params.start_url_id); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /portiaui/app/services/capabilities.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | ajax: Ember.inject.service(), 5 | 6 | fetchCapabilities: Ember.on('init', function() { 7 | this.get('ajax').request('/server_capabilities').then(capabilities => { 8 | this.setProperties(capabilities); 9 | }, () => { 10 | Ember.run.later(this, this.fetchCapabilities, 5000); 11 | }); 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /portiaui/app/services/clock.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | // based on an idea from https://www.rvdh.de/2014/11/14/time-based-triggers-in-ember-js/ 4 | export default Ember.Service.extend({ 5 | time: new Date(), 6 | 7 | metronome: Ember.on('init', function() { 8 | const now = new Date(); 9 | const interval = 1000 - (+now % 1000); 10 | this.set('time', now); 11 | 12 | Ember.run.later(this, this.metronome, interval); 13 | }) 14 | }); 15 | -------------------------------------------------------------------------------- /portiaui/app/services/overlays.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | counter: 0, 5 | 6 | hasOverlays: Ember.computed.bool('counter'), 7 | 8 | add() { 9 | this.incrementProperty('counter'); 10 | }, 11 | 12 | remove() { 13 | this.decrementProperty('counter'); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /portiaui/app/services/saving-notification.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed, inject: { service } } = Ember; 3 | 4 | export default Ember.Service.extend({ 5 | extractedItems: service(), 6 | 7 | counter: 0, 8 | lastSaved: null, 9 | 10 | isSaving: computed.bool('counter'), 11 | 12 | start() { 13 | this.get('extractedItems').activateExtraction(); 14 | this.incrementProperty('counter'); 15 | }, 16 | 17 | end() { 18 | this.decrementProperty('counter'); 19 | const counter = this.get('counter'); 20 | if (!counter) { 21 | this.set('lastSaved', new Date()); 22 | } 23 | this.get('extractedItems').update(); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /portiaui/app/storages/cookies.js: -------------------------------------------------------------------------------- 1 | import StorageObject from 'ember-local-storage/local/object'; 2 | 3 | export default StorageObject.extend(); 4 | -------------------------------------------------------------------------------- /portiaui/app/storages/page-loads.js: -------------------------------------------------------------------------------- 1 | import StorageObject from 'ember-local-storage/local/object'; 2 | 3 | export default StorageObject.extend(); 4 | -------------------------------------------------------------------------------- /portiaui/app/storages/ui-state-collapsed-panels.js: -------------------------------------------------------------------------------- 1 | import StorageObject from 'ember-local-storage/local/object'; 2 | 3 | export default StorageObject.extend(); 4 | -------------------------------------------------------------------------------- /portiaui/app/storages/ui-state-selected-tools.js: -------------------------------------------------------------------------------- 1 | import StorageObject from 'ember-local-storage/local/object'; 2 | 3 | const ToolStorage = StorageObject.extend({ 4 | init() { 5 | this._super(...arguments); 6 | 7 | // clear the next click selection mode if magic tool is active 8 | if (this.get('magicToolActive')) { 9 | this.set('selectionMode', null); 10 | } 11 | } 12 | }); 13 | 14 | ToolStorage.reopenClass({ 15 | initialState() { 16 | return { 17 | magicToolActive: true, 18 | selectionMode: null 19 | }; 20 | } 21 | }); 22 | 23 | export default ToolStorage; 24 | -------------------------------------------------------------------------------- /portiaui/app/styles/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeOut { 2 | 99% { 3 | display: block; 4 | opacity: 0; 5 | } 6 | } 7 | 8 | @keyframes hideDelay { 9 | from { 10 | display: inherit; 11 | } 12 | 13 | 99% { 14 | display: inherit; 15 | } 16 | 17 | to { 18 | display: none; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /portiaui/app/styles/_lib_config.scss: -------------------------------------------------------------------------------- 1 | $animation-time: .15s; 2 | $animation-easing: ease-in-out; 3 | $animation-easing-in: ease-in; 4 | $animation-easing-out: ease-out; 5 | 6 | // font awesome settings 7 | $fa-font-path: 'fonts'; 8 | -------------------------------------------------------------------------------- /portiaui/app/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $sidebar-width: 331px; 3 | 4 | // tree list 5 | $tree-list-row-height: 30px !default; 6 | $tree-list-icon-width: $tree-list-row-height !default; 7 | 8 | // panels 9 | $sidebar-background-color: $navbar-default-bg; 10 | $panel-padding-y: 10px; 11 | $panel-content-min-height: $tree-list-row-height * 3; 12 | $panel-min-height: $panel-content-min-height + $panel-padding-y * 2; 13 | 14 | // typography 15 | $space-width: 0.285em; 16 | 17 | // icons 18 | $pip-size: 3px; 19 | $icon-fade-opacity: 0.25; 20 | 21 | // colors 22 | $list-heading-color: $brand-danger; 23 | $pip-color: $panel-default-border; 24 | $panel-bg: darken($navbar-default-bg, 1.5%); 25 | $light-gray: #999; 26 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/animation-container.scss: -------------------------------------------------------------------------------- 1 | .animation-container { 2 | opacity: 1; 3 | overflow: visible; 4 | transition: opacity $animation-time $animation-easing $animation-time; 5 | 6 | &.inline { 7 | display: inline-block; 8 | } 9 | 10 | &.fade { 11 | opacity: 0; 12 | transition-delay: 0s; 13 | pointer-events: none; 14 | } 15 | } 16 | 17 | .animation-content { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | 22 | &[style^="transform"] { 23 | transition: transform $animation-time $animation-easing; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/browser-iframe.scss: -------------------------------------------------------------------------------- 1 | .browser-iframe { 2 | filter: none; 3 | opacity: 1; 4 | transition: filter ($animation-time * 2) $animation-easing, opacity ($animation-time * 2) $animation-easing; 5 | 6 | &.has-overlays { 7 | opacity: .5; 8 | 9 | @supports (filter: grayscale(100%)) { 10 | filter: grayscale(100%); 11 | opacity: 1; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/conflicts.scss: -------------------------------------------------------------------------------- 1 | 2 | .topbar-conflicts { 3 | margin-top: 7px; 4 | } 5 | 6 | .conflicts-text { 7 | padding: 100px; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/dropdown-delete.scss: -------------------------------------------------------------------------------- 1 | li.dropdown-delete { 2 | cursor: pointer; 3 | a { 4 | color: $brand-danger; 5 | &:hover { color: $brand-danger; } 6 | } 7 | 8 | &.is-confirmed { 9 | background-color: $brand-danger; 10 | a { 11 | color: white; 12 | &:hover { 13 | background-color: $brand-danger; 14 | color: white; 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/dropdown-menu.scss: -------------------------------------------------------------------------------- 1 | .dropdown-menu { 2 | outline: 0; 3 | margin: 2px 0; 4 | max-height: 200px; 5 | overflow-y: scroll; 6 | 7 | > .focused:not(.active) 8 | > a { 9 | &, 10 | &:hover, 11 | &:focus { 12 | text-decoration: none; 13 | color: $dropdown-link-hover-color; 14 | background-color: $dropdown-link-hover-bg; 15 | } 16 | } 17 | 18 | .icon { 19 | display: inline-block; 20 | width: $tree-list-icon-width; 21 | height: $line-height-computed; 22 | margin-left: -6px; 23 | margin-right: 10px; 24 | line-height: $line-height-computed; 25 | text-align: center; 26 | vertical-align: middle; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/dropdown-widget.scss: -------------------------------------------------------------------------------- 1 | .dropdown-menu-floating { 2 | display: none; 3 | 4 | &.open { 5 | display: block; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/extractor-options.scss: -------------------------------------------------------------------------------- 1 | .extractor-options { 2 | &.tree-list { 3 | > .tree-list-item { 4 | > .tree-list-item-row { 5 | margin-top: $line-height-computed; 6 | margin-bottom: ($line-height-computed / 2); 7 | } 8 | 9 | > .tree-list { 10 | padding-left: 0; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/fragment-options.scss: -------------------------------------------------------------------------------- 1 | form.fragment-form { 2 | margin-bottom: 10px; 3 | } 4 | 5 | .fragment-form { 6 | .compact-control { 7 | padding-left: 8px; 8 | padding-right: 8px; 9 | } 10 | 11 | .fragment-input { 12 | width: 125px; 13 | margin-left: 8px; 14 | margin-right: 10px; 15 | } 16 | 17 | .fragment-type { 18 | width: 75px; 19 | .value { 20 | max-width: calc(100% - 10px); 21 | } 22 | } 23 | 24 | .fragment-left-half { 25 | width: 52px; 26 | margin-left: 8px; 27 | margin-right: 4px; 28 | } 29 | 30 | .fragment-right-half { 31 | width: 52px; 32 | margin-left: 4px; 33 | margin-right: 8px; 34 | } 35 | } 36 | 37 | .fragment-action-icon { 38 | position: absolute; 39 | right: -10px; 40 | line-height: 35px; 41 | } 42 | 43 | .fragment-error { 44 | margin: 0 4px 0 4px; 45 | } 46 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/help-icon.scss: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | &.help-icon { 3 | pointer-events: auto; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/indentation-spacer.scss: -------------------------------------------------------------------------------- 1 | .indentation-spacer { 2 | display: inline-block; 3 | min-width: 20px; 4 | &.is-small { 5 | width: 10px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/input-with-clear.scss: -------------------------------------------------------------------------------- 1 | .input-with-clear { 2 | input { 3 | padding-right: 34px; 4 | } 5 | } 6 | 7 | .clear-input { 8 | position: absolute; 9 | right: 11px; 10 | top: 0; 11 | bottom: 0; 12 | height: 14px; 13 | line-height: 14px; 14 | margin: auto; 15 | color: #ccc; 16 | z-index: 3; 17 | } 18 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/list-item-badge.scss: -------------------------------------------------------------------------------- 1 | .list-item-badge { 2 | display: inline-block; 3 | flex: 0 0 20px; 4 | text-align: center; 5 | 6 | .badge { 7 | vertical-align: baseline; 8 | max-width: 26px; 9 | transition: background-color $animation-time $animation-easing; 10 | } 11 | 12 | .badge-centered { 13 | display: inline-block; 14 | margin: 0 -1000%; 15 | white-space: nowrap; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/list-item-combo.scss: -------------------------------------------------------------------------------- 1 | .list-item-combo { 2 | flex: 1 1 auto; 3 | min-width: 0; 4 | 5 | .list-item-editable + &, 6 | & + & { 7 | flex-grow: 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/list-item-editable.scss: -------------------------------------------------------------------------------- 1 | .list-item-editable { 2 | display: inline-flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | align-items: center; 6 | flex: 1 1 auto; 7 | min-width: 90px; 8 | margin-left: 5px; 9 | 10 | > span { 11 | flex: 0 1 auto; 12 | white-space: nowrap; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | } 16 | 17 | .fa-pencil { 18 | flex: 0 0 $tree-list-icon-width; 19 | color: $navbar-default-color; 20 | cursor: pointer; 21 | opacity: $icon-fade-opacity; 22 | transition: opacity $animation-time $animation-easing; 23 | text-align: center; 24 | } 25 | 26 | &:hover { 27 | .fa-pencil { 28 | opacity: 1; 29 | } 30 | } 31 | 32 | input { 33 | flex: 1 1 100%; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/list-item-icon.scss: -------------------------------------------------------------------------------- 1 | .list-item-icon { 2 | width: $tree-list-icon-width; 3 | flex: 0 0 $tree-list-icon-width; 4 | 5 | &.has-action { 6 | opacity: $icon-fade-opacity; 7 | 8 | &.active, 9 | .dropdown.open &, 10 | &:hover:not(.disabled) { 11 | opacity: 1; 12 | } 13 | } 14 | 15 | .tree-list-item-content & { 16 | height: $tree-list-row-height; 17 | line-height: $tree-list-row-height; 18 | } 19 | 20 | .tree-list-item-content a & { 21 | color: $navbar-default-color; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/list-item-selectable.scss: -------------------------------------------------------------------------------- 1 | .list-item-selectable { 2 | display: inline-flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | align-items: center; 6 | flex: 0 1 auto; 7 | 8 | .list-item-icon + &, 9 | .list-item-badge + & { 10 | flex-grow: 1; 11 | } 12 | 13 | &:not(.selecting) { 14 | > span { 15 | flex: 0 1 auto; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | } 20 | 21 | .caret { 22 | flex: 0 0 auto; 23 | margin: 0 (($tree-list-icon-width - 8px) / 2); 24 | color: $navbar-default-color; 25 | opacity: $icon-fade-opacity; 26 | } 27 | 28 | &:hover { 29 | .caret { 30 | opacity: 1; 31 | } 32 | } 33 | } 34 | 35 | .select-box { 36 | min-width: 60px; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/list-item-text.scss: -------------------------------------------------------------------------------- 1 | .list-item-text { 2 | display: inline; 3 | flex: 1 1 auto; 4 | padding: 0; 5 | overflow: hidden; 6 | white-space: nowrap; 7 | text-overflow: ellipsis; 8 | 9 | &.title { 10 | text-transform: uppercase; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/notifications.scss: -------------------------------------------------------------------------------- 1 | .notifications { 2 | $notification-top: $grid-gutter-width / 2; 3 | 4 | position: absolute; 5 | top: $notification-top; 6 | width: 100%; 7 | pointer-events: none; 8 | z-index: 3; 9 | 10 | .notification { 11 | margin-left: auto; 12 | margin-right: auto; 13 | max-width: 800px; 14 | 15 | &:first-of-type { 16 | margin-top: -15px; 17 | border-top: none; 18 | border-top-left-radius: 0; 19 | border-top-right-radius: 0; 20 | } 21 | 22 | > button { 23 | pointer-events: auto; 24 | outline: 0; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/page-actions.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/styles/components/page-actions.scss -------------------------------------------------------------------------------- /portiaui/app/styles/components/project-structure-spider-generation-url.scss: -------------------------------------------------------------------------------- 1 | .generated-url { 2 | color: $navbar-default-color; 3 | margin-left: $space-width; 4 | } 5 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/regex-pattern-list.scss: -------------------------------------------------------------------------------- 1 | .regex-pattern-list { 2 | .new-pattern { 3 | > div { 4 | display: flex; 5 | flex-direction: row; 6 | flex-wrap: nowrap; 7 | align-items: center; 8 | } 9 | 10 | i { 11 | width: $tree-list-row-height; 12 | flex: 0 0 $tree-list-row-height; 13 | height: $tree-list-row-height; 14 | line-height: $tree-list-row-height; 15 | text-align: center; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/save-status.scss: -------------------------------------------------------------------------------- 1 | @keyframes saving-blink { 2 | 0% { 3 | opacity: .2; 4 | } 5 | 50% { 6 | opacity: 1; 7 | } 8 | 100% { 9 | opacity: .2; 10 | } 11 | } 12 | 13 | .save-status { 14 | .label { 15 | display: inline-block; 16 | min-width: 60%; 17 | cursor: default; 18 | transition: background-color ($animation-time * 2) $animation-easing; 19 | 20 | span { 21 | animation-name: saving-blink; 22 | animation-duration: 1.4s; 23 | animation-iteration-count: infinite; 24 | animation-fill-mode: both; 25 | 26 | &:nth-child(2) { 27 | animation-delay: .2s; 28 | } 29 | 30 | &:nth-child(3) { 31 | animation-delay: .4s; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/show-links-legend.scss: -------------------------------------------------------------------------------- 1 | #show-links-legend { 2 | .list-item-badge { 3 | margin-right: $space-width; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/sliding-main.scss: -------------------------------------------------------------------------------- 1 | #window { 2 | overflow: hidden; 3 | } 4 | 5 | main { 6 | transition: transform $animation-time $animation-easing; 7 | transform: none; 8 | 9 | &.slide-right { 10 | transform: translateX($sidebar-width); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/start-url-options.scss: -------------------------------------------------------------------------------- 1 | .start-url-list { 2 | overflow-y: auto; 3 | min-height: 200px; 4 | margin-bottom: 5px; 5 | } 6 | 7 | .start-url-list-title { 8 | margin-bottom: 0; 9 | } 10 | 11 | .start-url-generation-list { 12 | p { 13 | white-space: nowrap; 14 | user-select: text; 15 | position: relative; 16 | margin: 0; 17 | } 18 | } 19 | 20 | .fragments-title { 21 | line-height: 30px; 22 | margin-bottom: 10px; 23 | display: inline-block; 24 | color: $navbar-default-color; 25 | } 26 | 27 | #add-fragment-button { 28 | position: absolute; 29 | top: 0; 30 | right: 0; 31 | line-height: 30px; 32 | } 33 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/tool-panel.scss: -------------------------------------------------------------------------------- 1 | .tool-panel { 2 | padding-top: $panel-padding-y; 3 | padding-bottom: $panel-padding-y; 4 | 5 | > div { 6 | position: relative; 7 | } 8 | 9 | h3 { 10 | display: inline-block; 11 | margin-top: 0; 12 | color: $navbar-default-color; 13 | font-size: $font-size-base; 14 | line-height: $tree-list-row-height; 15 | overflow: hidden; 16 | white-space: nowrap; 17 | text-overflow: ellipsis; 18 | text-transform: uppercase; 19 | } 20 | 21 | form { 22 | margin-bottom: $line-height-computed; 23 | 24 | label { 25 | font-weight: normal; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/tooltip-container.scss: -------------------------------------------------------------------------------- 1 | .tooltip-content { 2 | @extend .tooltip-inner; 3 | 4 | > p { 5 | margin: ($line-height-computed / 2) 0 0; 6 | text-align: left; 7 | 8 | &.first { 9 | margin-top: 0; 10 | } 11 | } 12 | 13 | em { 14 | font-style: normal; 15 | text-decoration: underline; 16 | } 17 | 18 | .tooltip-wide > & { 19 | max-width: $tooltip-max-width * 1.5; 20 | } 21 | } 22 | 23 | .tooltip-for { 24 | pointer-events: auto; 25 | } 26 | -------------------------------------------------------------------------------- /portiaui/app/styles/components/top-bar.scss: -------------------------------------------------------------------------------- 1 | #top-bar { 2 | margin-bottom: 0; 3 | z-index: 998; 4 | 5 | &:before, 6 | &:after { 7 | display: none; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /portiaui/app/styles/document.scss: -------------------------------------------------------------------------------- 1 | body { 2 | user-select: none; 3 | } 4 | -------------------------------------------------------------------------------- /portiaui/app/styles/droplet.scss: -------------------------------------------------------------------------------- 1 | .droplet { 2 | width: 30px; 3 | height: 30px; 4 | margin: 100px auto; 5 | position: absolute; 6 | top: -100px; 7 | right: 0; 8 | z-index: -1; 9 | background-color: #333; 10 | 11 | border-radius: 100%; 12 | -webkit-animation: sk-scaleout 2s infinite ease-in-out; 13 | animation: sk-scaleout 2s infinite ease-in-out; 14 | } 15 | 16 | @-webkit-keyframes sk-scaleout { 17 | 0% { -webkit-transform: scale(0) } 18 | 100% { 19 | -webkit-transform: scale(1.0); 20 | opacity: 0; 21 | } 22 | } 23 | 24 | @keyframes sk-scaleout { 25 | 0% { 26 | -webkit-transform: scale(0); 27 | transform: scale(0); 28 | } 100% { 29 | -webkit-transform: scale(1.0); 30 | transform: scale(1.0); 31 | opacity: 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /portiaui/app/styles/generic.scss: -------------------------------------------------------------------------------- 1 | .txt-describe { 2 | color: $light-gray; 3 | } 4 | 5 | .spaced { 6 | margin: 20px 0 20px 0; 7 | } 8 | 9 | .has-error { 10 | border-color: #a94442; 11 | } 12 | 13 | .mid-align { 14 | text-align: center; 15 | } 16 | 17 | .one-half-x { 18 | font-size: 1.5em; 19 | } 20 | 21 | .twice-x { 22 | font-size: 2em; 23 | } 24 | 25 | .full-width { 26 | width: 100%; 27 | } 28 | 29 | .very-opaque { 30 | opacity: 0.3; 31 | } 32 | 33 | .flex-center { 34 | justify-content: center; 35 | } 36 | 37 | .with-cursor { 38 | cursor: pointer; 39 | } 40 | -------------------------------------------------------------------------------- /portiaui/app/styles/layout/_clickable.scss: -------------------------------------------------------------------------------- 1 | .clickable, 2 | [data-ember-action] { 3 | cursor: pointer; 4 | } 5 | 6 | .ignore-active { 7 | &.active { 8 | pointer-events: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /portiaui/app/styles/layout/_forms.scss: -------------------------------------------------------------------------------- 1 | .input-list-item { 2 | $tree-list-caret-padding-y: ($tree-list-row-height - $line-height-computed) / 2; 3 | height: $tree-list-row-height - 4px; 4 | padding: ($padding-base-vertical - $tree-list-caret-padding-y) $padding-xs-horizontal; 5 | } 6 | -------------------------------------------------------------------------------- /portiaui/app/styles/layout/_full-page-content.scss: -------------------------------------------------------------------------------- 1 | .full-page-content { 2 | display: flex; 3 | flex-direction: column; 4 | flex-wrap: nowrap; 5 | justify-content: center; 6 | align-items: center; 7 | align-content: center; 8 | 9 | img[alt="Portia logo"] { 10 | display: block; 11 | flex: 0 0 auto; 12 | width: 25%; 13 | margin: 0 auto; 14 | } 15 | 16 | > h3 { 17 | margin: 2em auto 0; 18 | } 19 | 20 | > p { 21 | margin: 1em auto 0; 22 | } 23 | 24 | &:before, 25 | &:after { 26 | content: ''; 27 | display: block; 28 | } 29 | 30 | &:before { 31 | flex: 1 1 auto; 32 | } 33 | 34 | &:after { 35 | flex: 2 1 auto; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /portiaui/app/styles/templates/application.scss: -------------------------------------------------------------------------------- 1 | #window { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | flex-wrap: nowrap; 6 | 7 | > section { 8 | position: relative; 9 | flex: 1 1 auto; 10 | display: flex; 11 | flex-direction: row; 12 | flex-wrap: nowrap; 13 | overflow-x: hidden; 14 | overflow-y: auto; 15 | } 16 | } 17 | 18 | main { 19 | order: 0; 20 | flex: 1 1 auto; 21 | display: flex; 22 | flex-direction: row; 23 | flex-wrap: nowrap; 24 | align-items: stretch; 25 | } 26 | -------------------------------------------------------------------------------- /portiaui/app/styles/templates/browsers.scss: -------------------------------------------------------------------------------- 1 | .browser-list-container { 2 | @extend .full-page-content; 3 | 4 | margin: 0 auto; 5 | margin-top: 20px; 6 | padding: 0 40px; 7 | text-align: center; 8 | 9 | h3 { margin-top: 0; } 10 | } 11 | 12 | .browser-p { 13 | font-size: 1.3em; 14 | color: #777; 15 | } 16 | 17 | .browser-mg { 18 | margin: none; 19 | margin-left: 20px; 20 | margin-right: 20px; 21 | } 22 | 23 | .browser-logos { 24 | display: flex; 25 | margin-top: 26px; 26 | max-height: 120px; 27 | } 28 | 29 | .browser-logo { 30 | width: 120px; 31 | height: 120px; 32 | transition: 0.2s; 33 | &:hover { 34 | transform: translateY(-8px); 35 | } 36 | } 37 | 38 | .no-decoration { 39 | &:focus, &:hover { 40 | text-decoration: none; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /portiaui/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{loading-slider isLoading=loading duration=200}} 3 | 14 | 15 |
16 | {{notification-container}} 17 | {{outlet 'side-bar'}} 18 | {{#sliding-main}} 19 | {{outlet 'options-panels'}} 20 | {{outlet 'main'}} 21 | {{outlet 'tool-panels'}} 22 | {{/sliding-main}} 23 |
24 |
25 | -------------------------------------------------------------------------------- /portiaui/app/templates/branding.hbs: -------------------------------------------------------------------------------- 1 | 2 | Portia beta 3 | 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/browsers.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Unfortunately your browser doesn't support some of the features required to give you a great experience with Portia.

3 |

Please try using an up-to-date version of one of these browsers, which are known to work well with Portia.

4 | 5 | {{browser-list browsers=model}} 6 |
7 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /portiaui/app/templates/components/add-start-url-button.hbs: -------------------------------------------------------------------------------- 1 | {{#tooltip-container tooltipFor="start-url-button" tooltipContainer='body' as |options|}} 2 | {{#if (eq options.section 'tooltip')}} 3 | Toggle start page 4 | {{#if newStartUrl}} 5 |

6 | Add this page as a start page for your spider 7 |

8 | {{else}} 9 |

10 | Remove this page from your spider's start pages 11 |

12 | {{/if}} 13 | {{else}} 14 | 17 | {{/if}} 18 | {{/tooltip-container}} 19 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/animation-container.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{yield}} 3 |
4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/browser-iframe.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/templates/components/browser-iframe.hbs -------------------------------------------------------------------------------- /portiaui/app/templates/components/browser-list.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#each browsers as |browser|}} 3 | 4 | 5 |

{{browser.name}}

6 |
7 | {{/each}} 8 |
9 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/browser-url-blocked.hbs: -------------------------------------------------------------------------------- 1 | Portia is having trouble loading this page at the moment. Try a different page or try again later. 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/browser-url-failing.hbs: -------------------------------------------------------------------------------- 1 | Portia is currently having trouble loading this page would you like to try again? 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/buffered-input.hbs: -------------------------------------------------------------------------------- 1 | {{input id=inputId type=type class=(concat "form-control " (if focused "focused ") class) value=(mut displayedValue) enter=(if focused (action 'endEditing' 'enter')) escape-press=(if focused (action 'cancelEditing')) focus-in=(action 'startEditing') bubbles=true focus-out=(if focused (action 'endEditing' 'focus-out')) placeholder=placeholder autofocus=autofocus disabled=disabled spellcheck=spellcheck}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/colored-badge.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}}{{yield}}{{else}}{{value}}{{/if}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/colored-span.hbs: -------------------------------------------------------------------------------- 1 | {{~ yield ~}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/create-project-button.hbs: -------------------------------------------------------------------------------- 1 | {{#if canCreateProjects}} 2 |

3 |
4 | {{input type="text" class="form-control" value=(mut projectName) placeholder="Create a new project" 5 | enter=(action 'addProject')}} 6 | 7 | 10 | 11 |
12 | {{#if projects}} 13 |
OR
14 | {{/if}} 15 | {{/if}} 16 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/create-spider-button.hbs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/dropdown-delete.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{list-item-icon class="icon" icon="remove"}}{{if isConfirmed 'Are you sure?' text}} 3 | 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/dropdown-divider.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/templates/components/dropdown-divider.hbs -------------------------------------------------------------------------------- /portiaui/app/templates/components/dropdown-header.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/dropdown-item.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if hasBlock}} 3 | {{yield value}} 4 | {{else}} 5 | {{value}} 6 | {{/if}} 7 | 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/dropdown-menu.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash menu=this header=(component 'dropdown-header') item=(component 'dropdown-item' menu=this) divider=(component 'dropdown-divider'))}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/dropdown-widget.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash section='widget' openMenu=(action 'openMenu') closeMenu=(action 'closeMenu') toggleMenu=(action 'toggleMenu') focusIn=(action 'focusIn') focusOut=(action 'focusOut') keyDown=(action 'keyDown'))}} 2 | {{#dropdown-menu class=menuClasses events=events keyNavigate=keyNavigate active=(mut active) focused=(mut focused) orderItemsForSearch=orderItemsForSearch valuesEqual=valuesEqual onFocusIn=(action 'focusIn') onFocusOut=(action 'focusOut') as |options|}} 3 | {{#if open}} 4 | {{yield (hash section='menu' menu=options.menu header=options.header item=options.item divider=options.divider openMenu=(action 'openMenu') closeMenu=(action 'closeMenu') toggleMenu=(action 'toggleMenu') focusIn=(action 'focusIn') focusOut=(action 'focusOut') keyDown=(action 'keyDown'))}} 5 | {{/if}} 6 | {{/dropdown-menu}} 7 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/element-overlay.hbs: -------------------------------------------------------------------------------- 1 | {{#each rects key="@index" as |rect index|}} 2 | {{element-rect-overlay index=index icon=icon color=color positionMode=positionMode class=class overlay=this}} 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/element-rect-overlay.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{icon-button icon=icon}} 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/extracted-items-group.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="extracted-items-group" as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="extracted-items"}} 4 | {{#extracted-items-tab}} 5 | Extracted items 6 | {{/extracted-items-tab}} 7 | {{/group.tab}} 8 | {{#group.tab toolId="extracted-items-json"}} 9 | JSON 10 | {{/group.tab}} 11 | {{extracted-items-status}} 12 | {{else if (eq group.section "panels")}} 13 | {{#group.panel class="extracted-items container-fluid" toolId="extracted-items" as |active|}} 14 | {{extracted-items-panel selected=active}} 15 | {{/group.panel}} 16 | {{#group.panel class="extracted-items-json" toolId="extracted-items-json" as |active|}} 17 | {{extracted-items-json-panel selected=active}} 18 | {{/group.panel}} 19 | {{/if}} 20 | {{/tool-group}} 21 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/extracted-items-json-panel.hbs: -------------------------------------------------------------------------------- 1 | a 2 | {{#if selected ~}} 3 | {{extracted-items-json json=extractedItems.items position=0}} 4 | {{~/if}} 5 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/extracted-items-json-value.hbs: -------------------------------------------------------------------------------- 1 | {{#if fromArray}}{{depthSpaces}}{{/if}} 2 | {{escapedValue}}{{comma}} 3 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/extracted-items-panel.hbs: -------------------------------------------------------------------------------- 1 | {{#if isExtracting}} 2 |
3 | 4 |
5 |
Extracting data...
6 | {{/if}} 7 | 8 | {{#if failedExtraction}} 9 |
10 | 11 |
12 |
13 | {{failedMsg}} 14 |
15 | {{/if}} 16 | 17 | {{#if (and selected (not isExtracting))}} 18 | {{#each extractedItems.items key='@index' as |item|}} 19 | {{extracted-item-table item=item}} 20 | {{/each}} 21 | {{/if}} 22 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/extracted-items-status.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to changeInfo.path changeInfo.model bubbles=false}} 2 | {{#help-icon icon=icon placement='left'}} 3 | {{#if hasWarning}} 4 | {{changeInfo.text}} 5 | {{else}} 6 | Your sample is correctly configured for extraction 7 | {{/if}} 8 | {{/help-icon}} 9 | {{/link-to}} 10 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/extracted-items-tab.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | 3 | {{#unless isExtracting}} 4 | {{numItems}} 5 | {{/unless}} 6 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/feed-url-options.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Enter a publicly available URL containing newline separated URLs 4 | like this. 5 |

6 |
7 | 8 |
9 | 10 | {{input 11 | type="text" 12 | id='feedUrl' 13 | class="form-control focus-control" 14 | value=(mut startUrl.url) 15 | focus-out="saveFeedUrl" 16 | placeholder='https://gist.github.com/user/gist_id' 17 | }} 18 |
19 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/field-options.hbs: -------------------------------------------------------------------------------- 1 |

Field

2 |
3 |
4 | 7 | {{#help-icon}} 8 | Only extract items that have this field. All samples using this data format will be affected 9 | {{/help-icon}} 10 |
11 |
12 | 15 | {{#help-icon}} 16 | The value of this field will be ignored when checking for duplicate items 17 | {{/help-icon}} 18 |
19 |
20 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/help-icon.hbs: -------------------------------------------------------------------------------- 1 | {{#tooltip-container tooltipClasses=tooltipClasses tooltipFor=(concat "help-icon-" elementId) tooltipContainer=tooltipContainer placement=placement as |tooltip|}} 2 | {{#if (eq tooltip.section 'tooltip')}} 3 | {{yield}} 4 | {{else}} 5 | {{icon-button id=(concat "help-icon-" elementId) class=classes icon=icon}} 6 | {{/if}} 7 | {{/tooltip-container}} 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/icon-button.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/templates/components/icon-button.hbs -------------------------------------------------------------------------------- /portiaui/app/templates/components/input-with-clear.hbs: -------------------------------------------------------------------------------- 1 | {{input class="form-control" value=(mut value) placeholder=placeholder keyUp=(action "keyUp" on="key-up")}} 2 | {{icon-button class="clear-input" icon='close' action=(action 'clear')}} 3 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/link-crawling-options.hbs: -------------------------------------------------------------------------------- 1 |

Crawling rules

2 |
3 | {{#if (eq spider.linksToFollow 'all')}} 4 |
Following all encountered links within the same domains
5 | {{else if (eq spider.linksToFollow 'none')}} 6 |
Not following any links
7 | {{else if (eq spider.linksToFollow 'patterns')}} 8 |
Follow links that match these patterns
9 | {{regex-pattern-list list=spider.followPatterns onChange=(action 'save')}} 10 |
Exclude links that match these patterns
11 | {{regex-pattern-list list=spider.excludePatterns onChange=(action 'save')}} 12 | {{/if}} 13 | 14 |
15 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-add-annotation-menu.hbs: -------------------------------------------------------------------------------- 1 | {{#list-item-icon-menu icon='add-dropdown' as |options|}} 2 | {{#options.item value="Add annotation" action=(chain-actions (action 'addAnnotation') options.closeMenu) as |value|}} 3 | {{list-item-icon class="icon" icon="add"}}{{value}} 4 | {{/options.item}} 5 | {{#if allowNesting}} 6 | {{#options.item value="Add nested item" action=(chain-actions (action 'addNestedItem') options.closeMenu) as |value|}} 7 | {{list-item-icon class="icon" icon="add"}}{{value}} 8 | {{/options.item}} 9 | {{/if}} 10 | {{/list-item-icon-menu}} 11 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-annotation-field.hbs: -------------------------------------------------------------------------------- 1 | {{#list-item-relation-manager value=(mut annotation.field) choices=annotation.parent.schema.fields selecting=(mut selecting) onChange=(action 'changeField') validate=(action 'validateFieldName') create=(action 'addField') as |options|}} 2 | {{#if (eq options.section 'change-header')}} 3 | Type to change the field 4 | {{else if (eq options.section 'choices-header')}} 5 | Select an existing field 6 | {{else if (eq options.section 'choice')}} 7 | {{list-item-icon class="icon" icon=options.choice.type}}{{options.choice.name}} 8 | {{/if}} 9 | {{/list-item-relation-manager}} 10 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-badge.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#if hasBlock}}{{yield}}{{else}}{{value}}{{/if}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-editable.hbs: -------------------------------------------------------------------------------- 1 | {{#if editing}} 2 | {{buffered-input class="input-list-item" value=(mut value) focused=editing autoSelect=true spellcheck=spellcheck onChange=onChange validate=validate autofocus="autofocus"}} 3 | {{else}} 4 | {{value}} 5 | {{icon-button icon='edit' action=(action 'startEditing') bubbles=false}} 6 | {{/if}} 7 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-field-type.hbs: -------------------------------------------------------------------------------- 1 | {{#list-item-selectable value=(mut field.type) onChange=(action 'saveField') menuContainer="body" menuAlign="right" as |select|}} 2 | {{#if hasBlock}} 3 | {{#select.header}} 4 | {{yield}} 5 | {{/select.header}} 6 | {{/if}} 7 | {{#each types as |type|}} 8 | {{#select.item value=type action=(action select.setValueAndClose type)}} 9 | {{list-item-icon class="icon" icon=type}}{{type}} 10 | {{/select.item}} 11 | {{/each}} 12 | {{/list-item-selectable}} 13 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-icon-menu.hbs: -------------------------------------------------------------------------------- 1 | {{#dropdown-widget class="list-item-icon" menuContainer="body" menuAlign="right" as |options|}} 2 | {{#if (eq options.section 'widget')}} 3 | {{list-item-icon onClick=(action 'clickIcon') icon=icon action=options.toggleMenu tabindex=-1}} 4 | {{else if (eq options.section 'menu')}} 5 | {{yield options}} 6 | {{/if}} 7 | {{/dropdown-widget}} 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-icon.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/app/templates/components/list-item-icon.hbs -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-item-schema.hbs: -------------------------------------------------------------------------------- 1 | {{#list-item-relation-manager value=(mut item.schema) choices=item.ownerSample.spider.project.schemas selecting=(mut selecting) onChange=(action 'changeSchema') create=(action 'addSchema') as |options|}} 2 | {{#if (eq options.section 'change-header')}} 3 | Type to change the data format 4 | {{else if (eq options.section 'choices-header')}} 5 | Select an existing data format 6 | {{else if (eq options.section 'choice')}} 7 | {{list-item-icon class="icon" icon='schema'}}{{options.choice.name}} 8 | {{/if}} 9 | {{/list-item-relation-manager}} 10 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-link-crawling.hbs: -------------------------------------------------------------------------------- 1 | {{#list-item-selectable value=(mut linksToFollow) valueAttribute='label' onChange=(action 'saveSpider') menuContainer="body" menuAlign="left" as |select|}} 2 | {{#select.header}} 3 | Change how links are crawled 4 | {{/select.header}} 5 | {{#each followPatternOptions as |option|}} 6 | {{#select.item value=option action=(action select.setValueAndClose option)}} 7 | {{option.label}} 8 | {{/select.item}} 9 | {{/each}} 10 | {{/list-item-selectable}} 11 | {{#help-icon}} 12 | {{linksToFollow.description}} 13 | {{/help-icon}} 14 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-selectable.hbs: -------------------------------------------------------------------------------- 1 | {{#if selecting}} 2 | {{#if hasBlock}} 3 | {{#select-box choices=choices value=(mut value) valueAttribute=valueAttribute open=(mut selecting) onChange=onChange buttonClass=(concat "input-list-item " buttonClass) menuClass=menuClass menuAlign=menuAlign menuContainer=menuContainer as |options|}} 4 | {{yield options}} 5 | {{/select-box}} 6 | {{else}} 7 | {{select-box choices=choices value=(mut value) valueAttribute=valueAttribute open=(mut selecting) onChange=onChange buttonClass=(concat "input-list-item " buttonClass) menuClass=menuClass menuAlign=menuAlign menuContainer=menuContainer}} 8 | {{/if}} 9 | {{else}} 10 | 11 | {{#if valueAttribute}} 12 | {{get value valueAttribute}} 13 | {{else}} 14 | {{value}} 15 | {{/if}} 16 | 17 | 18 | 19 | 20 | {{/if}} 21 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/list-item-text.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/notification-container.hbs: -------------------------------------------------------------------------------- 1 | {{#each banners as |notification|}} 2 | {{notification-message notification=notification fade=notification.fading fadeAction=(action 'fadeBanner' notification)}} 3 | {{/each}} 4 | 5 | {{#each displayNotifications as |notification|}} 6 | {{notification-message notification=notification fade=notification.fading closeAction=(action 'dismissNotification' notification) fadeAction=(action 'fadeNotification' notification)}} 7 | {{/each}} 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/notification-message.hbs: -------------------------------------------------------------------------------- 1 | {{#if closeAction}} 2 | 5 | {{/if}} 6 | {{#if title}} 7 |

{{title}}

8 | {{/if}} 9 |

{{message}}

10 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/project-structure-spider-feed-url.hbs: -------------------------------------------------------------------------------- 1 | {{#tree-list-item as |options|}} 2 | {{#link-to 'projects.project.spider.start-url.options' index bubbles=false}} 3 | {{indentation-spacer}} 4 | {{list-item-icon icon='url-feed'}} 5 | {{#list-item-text class="txt-describe"}}{{url}}{{/list-item-text}} 6 | {{#link-to 'projects.project.spider' bubbles=false}} 7 | {{#link-to 'projects.project.spider.start-url.options' index 8 | bubbles=false 9 | class="ignore-active" 10 | }} 11 | {{list-item-icon icon='options'}} 12 | {{/link-to}} 13 | {{/link-to}} 14 | {{list-item-icon icon='remove' action=(action removeStartUrl) bubbles=false}} 15 | {{/link-to}} 16 | {{/tree-list-item}} 17 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/project-structure-spider-generated-url.hbs: -------------------------------------------------------------------------------- 1 | {{#tree-list-item as |options|}} 2 | {{#link-to 'projects.project.spider.start-url.options' index bubbles=false}} 3 | {{indentation-spacer}} 4 | {{list-item-icon icon='url-generated'}} 5 | {{#list-item-text class="generated-url"}}{{url}}{{/list-item-text}} 6 | {{#link-to 'projects.project.spider' bubbles=false}} 7 | {{#link-to 'projects.project.spider.start-url.options' index bubbles=false class="ignore-active"}} 8 | {{list-item-icon icon='options'}} 9 | {{/link-to}} 10 | {{/link-to}} 11 | {{list-item-icon icon='remove' action=(action removeStartUrl) bubbles=false}} 12 | {{/link-to}} 13 | {{/tree-list-item}} 14 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/project-structure-spider-url.hbs: -------------------------------------------------------------------------------- 1 | {{#tree-list-item as |options|}} 2 | {{#link-to 'projects.project.spider' spider (query-params url=url baseurl=null) active=false}} 3 | {{indentation-spacer}} 4 | {{list-item-icon icon='url'}} 5 | {{list-item-editable value=(mut viewUrl) editing=(mut urlAdded) spellcheck=false}} 6 | {{list-item-icon icon='remove' action=(action removeStartUrl) bubbles=false}} 7 | {{/link-to}} 8 | {{/tree-list-item}} 9 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/save-status.hbs: -------------------------------------------------------------------------------- 1 | {{#tooltip-container tooltipFor=(concat "label-" elementId) tooltipContainer='body' as |tooltip|}} 2 | {{#if (eq tooltip.section 'tooltip')}} 3 |

Every change you make is automatically saved by Portia

4 | {{else}} 5 | 6 | {{#if isSaving}} 7 | Saving ... 8 | {{else if timeSinceLastSave}} 9 | Last saved {{timeSinceLastSave}} 10 | {{else}} 11 | Changes are saved automatically 12 | {{/if}} 13 | 14 | {{/if}} 15 | {{/tooltip-container}} 16 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/scrapinghub-links.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | Portia 2.0 Documentation 4 | 5 |
  • 6 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/show-links-button.hbs: -------------------------------------------------------------------------------- 1 | {{#tooltip-container tooltipFor="show-links-button" text="Toggle link highlighting" tooltipContainer='body' as |options|}} 2 | 5 | {{/tooltip-container}} 6 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/show-links-legend.hbs: -------------------------------------------------------------------------------- 1 | {{#tree-list}} 2 | {{#tree-list-item as |options|}} 3 | {{list-item-badge value=followedLinks color=colors.green}} 4 | Followed 5 | {{/tree-list-item}} 6 | {{#tree-list-item as |options|}} 7 | {{list-item-badge value=jsLinks color=colors.blue}} 8 | Followed when Javascript is enabled 9 | {{/tree-list-item}} 10 | {{#tree-list-item as |options|}} 11 | {{list-item-badge value=ignoredLinks color=colors.red}} 12 | Not Followed 13 | {{/tree-list-item}} 14 | {{/tree-list}} 15 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/sliding-main.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/spider-indentation.hbs: -------------------------------------------------------------------------------- 1 | {{indentation-spacer}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/spider-message.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasSpider}} 2 | {{#tooltip-container tooltipFor='run-spider-button' text='Run this spider.' tooltipContainer='body'}} 3 | {{list-item-icon id='run-spider-button' icon='play' action=(action 'runSpider' currentSpider)}} 4 | {{/tooltip-container}} 5 | {{/if}} -------------------------------------------------------------------------------- /portiaui/app/templates/components/start-url-options.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="fragments-options-group" collapsible=false onClose=(route-action 'closeOptions') as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="annotation-options"}}{{title}}{{/group.tab}} 4 | {{else if (eq group.section "panels")}} 5 | {{#group.panel class="extracted-items container-fluid" toolId="annotation-options" as |active|}} 6 | {{component startUrl.optionsComponentName 7 | spider=spider 8 | startUrl=startUrl 9 | saveSpider=saveSpider 10 | }} 11 | {{/group.panel}} 12 | {{/if}} 13 | {{/tool-group}} 14 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tool-group.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 17 |
    18 |
    19 | {{yield (hash section="panels" group=this tab=(component 'tool-tab' group=this) panel=(component 'tool-panel' group=this))}} 20 |
    21 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tool-panel.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{yield active}} 3 |
    4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tool-tab.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{yield active}} 3 | 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tooltip-container.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash section='body')}} 2 | 12 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tooltip-icon.hbs: -------------------------------------------------------------------------------- 1 | {{#tooltip-container 2 | tooltipFor=elementId 3 | text=text 4 | tooltipContainer='body' 5 | }} 6 | {{icon-button 7 | id=elementId 8 | icon=icon 9 | modifyClasses=modifyClasses 10 | action=(action 'onClick') 11 | }} 12 | {{/tooltip-container}} 13 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tree-list-item-row.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{yield}} 3 |
    4 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tree-list-item.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#tree-list-item-row 3 | isCentered=isCentered 4 | onMouseEnter=onMouseEnter 5 | onMouseLeave=onMouseLeave 6 | }} 7 | {{yield (hash section="item")}} 8 | {{/tree-list-item-row}} 9 | {{#if hasChildren}} 10 | {{#tree-list}} 11 | {{yield (hash section="subtrees")}} 12 | {{/tree-list}} 13 | {{/if}} 14 |
    15 | -------------------------------------------------------------------------------- /portiaui/app/templates/components/tree-list.hbs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/options-panels.hbs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects.hbs: -------------------------------------------------------------------------------- 1 |
    2 | Portia logo 3 | 4 |

    What would you like to work on today?

    5 | {{create-project-button projects=model}} 6 | {{#if model}} 7 |

    Choose a project from the list below.

    8 | 9 | {{project-list projects=model}} 10 | {{/if}} 11 |
    12 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project.hbs: -------------------------------------------------------------------------------- 1 | {{#browser-view-port clickHandler=(action "viewPortClick") as |options|}} 2 | {{#if (eq options.section "toolbar")}} 3 | {{outlet "browser-toolbar"}} 4 | {{else if (eq options.section "overlays")}} 5 | {{outlet "browser-overlays"}} 6 | {{/if}} 7 | {{/browser-view-port}} 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/conflicts/help.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#if conflictedFiles}} 3 |

    Aw, Snap!

    4 | 5 |

    Portia couldn't deploy the project because there are conflicts with another user's changes

    6 |

    Resolve the conflicts manually by selecting the conflicting files from the left panel to deploy the project.

    7 | {{else}} 8 |

    All done

    9 | 10 |

    11 | All conflicts are resolved, to continue, go back to the 12 | {{link-to "project page" 'projects.project' projectController.model}} 13 | and try to deploy again. 14 |

    15 | {{/if}} 16 |
    17 | 18 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/conflicts/resolver.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{json-file-compare json=model.contents update="updateConflict"}} 3 |
    4 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/conflicts/topbar.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/schema.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/schema/field.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/schema/field/options.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="field-options-group" collapsible=false onClose=(action 'closeOptions') as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="field-options"}} 4 | Field properties 5 | {{/group.tab}} 6 | {{else if (eq group.section "panels")}} 7 | {{#group.panel class="extracted-items container-fluid" toolId="field-options" as |active|}} 8 | {{field-options field=model}} 9 | {{/group.panel}} 10 | {{/if}} 11 | {{/tool-group}} 12 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/schema/structure.hbs: -------------------------------------------------------------------------------- 1 | {{schema-structure-listing schema=model}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/link-options.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="link-crawling-options-group" collapsible=false onClose=(action 'closeOptions') as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="link-crawling-options"}} 4 | Link crawling options 5 | {{/group.tab}} 6 | {{else if (eq group.section "panels")}} 7 | {{#group.panel class="extracted-items container-fluid" toolId="link-crawling-options" as |active|}} 8 | {{link-crawling-options spider=model}} 9 | {{/group.panel}} 10 | {{/if}} 11 | {{/tool-group}} 12 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/options.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="spider-options-group" collapsible=false onClose=(action 'closeOptions') as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="spider-options"}} 4 | Spider properties 5 | {{/group.tab}} 6 | {{else if (eq group.section "panels")}} 7 | {{#group.panel class="extracted-items container-fluid" toolId="spider-options" as |active|}} 8 | {{spider-options spider=model}} 9 | {{/group.panel}} 10 | {{/if}} 11 | {{/tool-group}} 12 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/overlays.hbs: -------------------------------------------------------------------------------- 1 | {{#if model.showLinks}} 2 |
    3 | {{#each linkOverlayElements key="guid" as |overlay|}} 4 | {{element-overlay viewPortElement=overlay.element color=overlay.color}} 5 | {{/each}} 6 |
    7 | {{/if}} 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/annotation/selection.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/data.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/data/annotation.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/data/annotation/options.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="annotation-options-group" collapsible=false onClose=(action 'closeOptions') as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="annotation-options"}} 4 | Annotation properties 5 | {{/group.tab}} 6 | {{else if (eq group.section "panels")}} 7 | {{#group.panel class="extracted-items container-fluid" toolId="annotation-options" as |active|}} 8 | {{annotation-options annotation=model}} 9 | {{field-options field=model.field.content}} 10 | {{extractor-options annotation=model}} 11 | {{/group.panel}} 12 | {{/if}} 13 | {{/tool-group}} 14 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/data/item.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/data/structure.hbs: -------------------------------------------------------------------------------- 1 | {{data-structure-listing sample=model annotationColors=annotationColors}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/data/tools.hbs: -------------------------------------------------------------------------------- 1 | {{#tool-group id="inspector-group" as |group|}} 2 | {{#if (eq group.section "tabs")}} 3 | {{#group.tab toolId="inspector"}} 4 | Inspector 5 | {{/group.tab}} 6 | {{else if (eq group.section "panels")}} 7 | {{#group.panel class="inspector container-fluid" toolId="inspector"}} 8 | {{inspector-panel sample=model annotationColors=annotationColors}} 9 | {{/group.panel}} 10 | {{/if}} 11 | {{/tool-group}} 12 | 13 | {{extracted-items-group}} 14 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/item.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/structure.hbs: -------------------------------------------------------------------------------- 1 | {{outlet 'sample-structure'}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/sample/toolbar.hbs: -------------------------------------------------------------------------------- 1 | {{outlet 'browser-toolbar'}} 2 | {{#tooltip-container tooltipFor="sample-close-button-browser" text="Finish editing your sample so you can continue browsing and see how it works on other pages" tooltipContainer='body'}} 3 | {{#link-to 'projects.project.spider' class="btn btn-primary" activeClass="" id="sample-close-button-browser"}} 4 | {{icon-button icon='close'}} Close sample 5 | {{/link-to}} 6 | {{/tooltip-container}} 7 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/start-url/options.hbs: -------------------------------------------------------------------------------- 1 | {{start-url-options 2 | spider = model.spider 3 | startUrlId = model.startUrlId 4 | }} 5 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/structure.hbs: -------------------------------------------------------------------------------- 1 | {{spider-structure-listing 2 | project=model.project 3 | spider=model 4 | closeOptions=(route-action 'closeOptions') 5 | transitionToFragments=(route-action 'transitionToFragments') 6 | }} 7 | {{outlet 'spider-structure'}} 8 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/spider/toolbar.hbs: -------------------------------------------------------------------------------- 1 | {{show-links-button spider=model}} 2 | {{add-start-url-button spider=model}} 3 | {{edit-sample-button spider=model}} 4 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/structure.hbs: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /portiaui/app/templates/projects/project/toolbar.hbs: -------------------------------------------------------------------------------- 1 | {{create-spider-button project=model}} 2 | -------------------------------------------------------------------------------- /portiaui/app/templates/tool-panels.hbs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /portiaui/app/transforms/array.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Transform.extend({ 4 | deserialize: function(serialized) { 5 | if (Array.isArray(serialized)) { 6 | return serialized; 7 | } 8 | return []; 9 | }, 10 | 11 | serialize: function(deserialized) { 12 | if (Array.isArray(deserialized)) { 13 | return deserialized; 14 | } 15 | return []; 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /portiaui/app/transforms/json.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Transform.extend({ 4 | deserialize: function(serialized) { 5 | return JSON.parse(serialized); 6 | }, 7 | 8 | serialize: function(deserialized) { 9 | return JSON.stringify(deserialized); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /portiaui/app/transforms/start-url.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import buildStartUrl from '../models/start-url'; 3 | 4 | export default DS.Transform.extend({ 5 | deserialize: function(serialized) { 6 | if (Array.isArray(serialized)) { 7 | return serialized.map((url) => buildStartUrl(url)); 8 | } 9 | return []; 10 | }, 11 | 12 | serialize: function(deserialized) { 13 | if (Array.isArray(deserialized)) { 14 | return deserialized.map((startUrl) => startUrl.serialize()); 15 | } 16 | return []; 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /portiaui/app/utils/attrs.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function attrValue(attr) { 4 | return (!Ember.isNone(attr) && typeof attr === 'object' && 'value' in attr) ? attr.value : attr; 5 | } 6 | 7 | export function attrChanged(oldAttrs, newAttrs, key) { 8 | return !oldAttrs || attrValue(oldAttrs[key]) !== attrValue(newAttrs[key]); 9 | } 10 | 11 | export function attrChangedTo(oldAttrs, newAttrs, key, value) { 12 | return attrChanged(oldAttrs, newAttrs, key) && attrValue(newAttrs[key]) === value; 13 | } 14 | 15 | export default { 16 | attrValue, 17 | attrChanged, 18 | attrChangedTo 19 | }; 20 | -------------------------------------------------------------------------------- /portiaui/app/utils/browser-features.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { RSVP } = Ember; 3 | 4 | export default function hasBrowserFeatures() { 5 | // generatedcontent: detection issue with zoom in chrome 6 | let features = [ 7 | "eventlistener", "json", "postmessage", "queryselector", "requestanimationframe", "svg", 8 | "websockets", "cssanimations", "csscalc", "flexbox", "nthchild", 9 | "csspointerevents", "opacity", "csstransforms", "csstransitions", "cssvhunit", 10 | "classlist", "placeholder", "localstorage", "svgasimg", "datauri", "atobbtoa" 11 | ]; 12 | let feature_promises = features.map((feature) => { 13 | return new RSVP.Promise((resolve) => { 14 | Modernizr.on(feature, (isFeatureActive) => { resolve(isFeatureActive); }); 15 | }); 16 | }); 17 | 18 | return RSVP.all(feature_promises); 19 | } 20 | -------------------------------------------------------------------------------- /portiaui/app/utils/computed.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function computedPropertiesEqual(a, b) { 4 | return Ember.computed(a, b, function() { 5 | return this.get(a) === this.get(b); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /portiaui/app/utils/ensure-promise.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | // http://stackoverflow.com/questions/28247401/how-can-i-test-if-a-function-is-returning-a-promise-in-ember 4 | export default function ensurePromise(x) { 5 | return new Ember.RSVP.Promise(function(resolve) { 6 | resolve(x); 7 | }); 8 | } -------------------------------------------------------------------------------- /portiaui/app/utils/promises.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function ensurePromise(valueOrPromise) { 4 | return new Ember.RSVP.Promise(function(resolve) { 5 | resolve(valueOrPromise); 6 | }); 7 | } 8 | 9 | export default { 10 | ensurePromise 11 | }; 12 | -------------------------------------------------------------------------------- /portiaui/app/utils/types.js: -------------------------------------------------------------------------------- 1 | export function toType(obj) { 2 | return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); 3 | } 4 | 5 | export function isObject(obj) { 6 | return toType(obj) === 'object'; 7 | } 8 | 9 | export function isArray(obj) { 10 | return Array.isArray(obj); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /portiaui/app/validations/fixed-fragment.js: -------------------------------------------------------------------------------- 1 | import { validatePresence } from 'ember-changeset-validations/validators'; 2 | import validateWhitespace from '../validators/whitespace'; 3 | 4 | export default { 5 | value: [ 6 | validatePresence({ presence: true, message: 'Should not be empty.'}), 7 | validateWhitespace() 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /portiaui/app/validations/list-fragment.js: -------------------------------------------------------------------------------- 1 | import { validatePresence } from 'ember-changeset-validations/validators'; 2 | 3 | export default { 4 | value: validatePresence(true) 5 | }; 6 | -------------------------------------------------------------------------------- /portiaui/app/validations/range-fragment.js: -------------------------------------------------------------------------------- 1 | import validateRange from '../validators/range'; 2 | 3 | export default { 4 | value: validateRange() 5 | }; 6 | -------------------------------------------------------------------------------- /portiaui/app/validators/whitespace.js: -------------------------------------------------------------------------------- 1 | export default function validateWhitespace() { 2 | return (key, newValue/*, oldValue, changes */) => { 3 | return newValue.match(/\s/g) ? 'Should not have whitespace' : true; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /portiaui/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portia-ui", 3 | "ignore": [ 4 | "*", 5 | "!bower.json" 6 | ], 7 | "dependencies": { 8 | "ember": "~2.6.0", 9 | "ember-cli-shims": "0.1.1", 10 | "ember-cli-test-loader": "0.2.2", 11 | "ember-qunit-notifications": "0.1.0", 12 | "jquery": "^2.2.0", 13 | "blob-polyfill": "~1.0.20150320", 14 | "cookie": "~1.1.0", 15 | "bootstrap-sass": "~3.3.6", 16 | "font-awesome": "~4.5.0", 17 | "jquery-color": "~2.1.2", 18 | "moment": "~2.11.2", 19 | "uri.js": "~1.16.0", 20 | "fetch": "~0.10.1", 21 | "es6-promise": "~3.0.2", 22 | "uri-templates": "~0.1.9", 23 | "css-escape": "~1.5.0", 24 | "animation-frame": "~0.2.4" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /portiaui/config/deprecation-workflow.js: -------------------------------------------------------------------------------- 1 | window.deprecationWorkflow = window.deprecationWorkflow || {}; 2 | window.deprecationWorkflow.config = { 3 | workflow: [ 4 | { handler: "silence", matchMessage: /You modified .+ twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 3.0/ } 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /portiaui/config/environment-development.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(ENV) { 4 | ENV.APP.LOG_ACTIVE_GENERATION = true; 5 | ENV.APP.LOG_TRANSITIONS = true; 6 | ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 7 | ENV.APP.LOG_VIEW_LOOKUPS = true; 8 | //ENV.APP.LOG_RESOLVER = true; 9 | return ENV; 10 | }; -------------------------------------------------------------------------------- /portiaui/config/environment-production.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(ENV) { 4 | return ENV; 5 | }; -------------------------------------------------------------------------------- /portiaui/config/environment-test.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(ENV) { 4 | // Testem prefers this... 5 | ENV.baseURL = '/'; 6 | ENV.locationType = 'none'; 7 | 8 | // keep test console output quieter 9 | ENV.APP.LOG_ACTIVE_GENERATION = false; 10 | ENV.APP.LOG_VIEW_LOOKUPS = false; 11 | 12 | ENV.APP.rootElement = '#ember-testing'; 13 | return ENV; 14 | }; -------------------------------------------------------------------------------- /portiaui/public/assets/images/chrome-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/public/assets/images/chrome-logo.jpg -------------------------------------------------------------------------------- /portiaui/public/assets/images/firefox-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/public/assets/images/firefox-logo.png -------------------------------------------------------------------------------- /portiaui/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /portiaui/public/empty-frame.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /portiaui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /portiaui/testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "PhantomJS" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /portiaui/tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /portiaui/tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /portiaui/tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /portiaui/tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let application; 7 | 8 | let attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /portiaui/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /portiaui/tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/tests/unit/.gitkeep -------------------------------------------------------------------------------- /portiaui/tests/unit/utils/start-urls-test.js: -------------------------------------------------------------------------------- 1 | import { multiplicityFragment } from '../../../utils/start-urls'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Utility | startUrls'); 5 | 6 | test('is correct for a one number range', function(assert) { 7 | const fragment = { type: 'range', value: '0-0' }; 8 | assert.equal(multiplicityFragment(fragment), 1); 9 | }); 10 | 11 | test('is correct for a large range', function(assert) { 12 | const fragment = { type: 'range', value: '0-99' }; 13 | assert.equal(multiplicityFragment(fragment), 100); 14 | }); 15 | 16 | test('is correct for a non-zero starting range', function(assert) { 17 | const fragment = { type: 'range', value: '51-100' }; 18 | assert.equal(multiplicityFragment(fragment), 50); 19 | }); 20 | -------------------------------------------------------------------------------- /portiaui/tests/unit/validators/whitespace-test.js: -------------------------------------------------------------------------------- 1 | import validateWhitespace from '../../../validators/whitespace'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Validators | validateWhitespace'); 5 | 6 | test('it should be true without whitespace', function(assert) { 7 | const key = 'value'; 8 | const validator = validateWhitespace(); 9 | 10 | assert.equal(validator(key, 'withoutspace'), true); 11 | }); 12 | 13 | test('it should not have whitespace', function(assert) { 14 | const error = 'Should not have whitespace'; 15 | const key = 'value'; 16 | const validator = validateWhitespace(); 17 | 18 | assert.equal(validator(key, 'with space'), error); 19 | assert.equal(validator(key, 'endspace '), error); 20 | assert.equal(validator(key, ' startspace'), error); 21 | }); 22 | -------------------------------------------------------------------------------- /portiaui/vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/portiaui/vendor/.gitkeep -------------------------------------------------------------------------------- /slybot/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | docs/_build 3 | slybot.egg-info/ 4 | -------------------------------------------------------------------------------- /slybot/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | include slybot/validation/schemas.json 4 | include slybot/splash-script-combined.js 5 | -------------------------------------------------------------------------------- /slybot/Makefile.buildbot: -------------------------------------------------------------------------------- 1 | build: 2 | bin/makedeb 3 | -------------------------------------------------------------------------------- /slybot/README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Slybot crawler 3 | ============== 4 | 5 | Slybot is a Python web crawler for doing web scraping. It's implemented on top of the 6 | `Scrapy`_ web crawling framework and the `Scrapely`_ extraction library. 7 | 8 | The documentation (including installation and usage) can be found at: 9 | http://slybot.readthedocs.org/ 10 | 11 | .. _Scrapely: https://github.com/scrapy/scrapely 12 | .. _Scrapy: http://scrapy.org 13 | -------------------------------------------------------------------------------- /slybot/bin/makedeb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | version=$(python setup.py --version)-r$(git log --oneline | wc -l)+$(date +%Y%m%d%H%M)~$(git rev-parse --short HEAD)${BUILD_CODE:+~$BUILD_CODE} 4 | debchange -m -D unstable --force-distribution -v $version "Automatic build" 5 | debuild --no-lintian -us -uc -b 6 | -------------------------------------------------------------------------------- /slybot/bin/slybot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | os.environ['SCRAPY_SETTINGS_MODULE'] = 'slybot.settings' 4 | 5 | from scrapy.cmdline import execute 6 | execute() 7 | -------------------------------------------------------------------------------- /slybot/debian/changelog: -------------------------------------------------------------------------------- 1 | python-slybot (0.9) unstable; urgency=low 2 | 3 | * Initial release. 4 | 5 | -- Scrapinghub Team Wed, 31 Oct 2012 16:32:13 -0300 6 | -------------------------------------------------------------------------------- /slybot/debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /slybot/debian/control: -------------------------------------------------------------------------------- 1 | Source: python-slybot 2 | Section: python 3 | Priority: extra 4 | Maintainer: Scrapinghub Team 5 | Build-Depends: debhelper (>= 7), python (>=2.7) 6 | Standards-Version: 3.8.3 7 | Homepage: https://github.com/scrapinghub/portia 8 | 9 | Package: python-slybot 10 | Architecture: all 11 | Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, 12 | scrapy (>= 1.0.3.post6), 13 | python-scrapely, 14 | python-loginform, 15 | python-lxml, 16 | python-dateparser, 17 | python-scrapy-splash, 18 | python-page-finder 19 | Description: A web crawler implemented in Python. 20 | Slybot is a Python web crawler for doing web scraping. It's implemented on top 21 | of the Scrapy web crawling framework and the Scrapely extraction library. 22 | -------------------------------------------------------------------------------- /slybot/debian/copyright: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2012 Scrapinghub 2 | -------------------------------------------------------------------------------- /slybot/debian/pyversions: -------------------------------------------------------------------------------- 1 | 2.5- 2 | -------------------------------------------------------------------------------- /slybot/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | -------------------------------------------------------------------------------- /slybot/requirements-clustering.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | page_clustering==0.0.1 3 | -------------------------------------------------------------------------------- /slybot/requirements-test.txt: -------------------------------------------------------------------------------- 1 | tox==3.12.1 2 | nose==1.3.7 3 | nose-timer==0.7.5 4 | doctest-ignore-unicode==0.1.2 5 | setuptools>=41.0.1 6 | -------------------------------------------------------------------------------- /slybot/requirements.txt: -------------------------------------------------------------------------------- 1 | # Slybot requirements 2 | numpy==1.16.4 3 | Scrapy==1.6.0 4 | scrapely==0.13.5 5 | loginform==1.2.0 6 | lxml==4.3.4 7 | dateparser==0.7.1 8 | python-dateutil==2.8.0 9 | jsonschema==2.6.0 10 | six==1.12.0 11 | scrapy-splash==0.7.2 12 | page_finder==0.1.8 13 | chardet==3.0.4 14 | -------------------------------------------------------------------------------- /slybot/scrapy.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | default = slybot.settings 3 | -------------------------------------------------------------------------------- /slybot/slybot/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.13.3' 2 | -------------------------------------------------------------------------------- /slybot/slybot/exporter.py: -------------------------------------------------------------------------------- 1 | from scrapy.exporters import CsvItemExporter 2 | from scrapy.conf import settings 3 | 4 | 5 | class SlybotCSVItemExporter(CsvItemExporter): 6 | def __init__(self, *args, **kwargs): 7 | kwargs['fields_to_export'] = settings.getlist('CSV_EXPORT_FIELDS') or None 8 | super(SlybotCSVItemExporter, self).__init__(*args, **kwargs) 9 | -------------------------------------------------------------------------------- /slybot/slybot/fieldtypes/images.py: -------------------------------------------------------------------------------- 1 | """Images.""" 2 | from scrapely.extractors import extract_image_url 3 | from slybot.fieldtypes.url import UrlFieldTypeProcessor 4 | 5 | 6 | class ImagesFieldTypeProcessor(UrlFieldTypeProcessor): 7 | name = 'image' 8 | description = 'extracts image URLs' 9 | 10 | def extract(self, text): 11 | if text is not None: 12 | return extract_image_url(text) or '' 13 | return '' 14 | -------------------------------------------------------------------------------- /slybot/slybot/fieldtypes/number.py: -------------------------------------------------------------------------------- 1 | """ 2 | Numeric data extraction 3 | """ 4 | from scrapely.extractors import contains_any_numbers, extract_number 5 | 6 | class NumberTypeProcessor(object): 7 | """NumberTypeProcessor 8 | 9 | Extracts a number from text 10 | 11 | >>> from scrapely.extractors import htmlregion 12 | >>> n = NumberTypeProcessor() 13 | >>> n.extract(htmlregion(u"there are no numbers here")) 14 | >>> n.extract(htmlregion(u"foo 34")) 15 | u'foo 34' 16 | >>> n.adapt(u"foo 34", None) 17 | u'34' 18 | 19 | If more than one number is present, nothing is extracted 20 | >>> n.adapt(u"34 48", None) is None 21 | True 22 | """ 23 | name = 'number' 24 | description = 'extracts a single number in the text passed' 25 | 26 | def extract(self, htmlregion): 27 | """Only matches and extracts strings with at least one number""" 28 | return contains_any_numbers(htmlregion.text_content) 29 | 30 | def adapt(self, text, htmlpage=None): 31 | return extract_number(text) 32 | -------------------------------------------------------------------------------- /slybot/slybot/fieldtypes/point.py: -------------------------------------------------------------------------------- 1 | 2 | class GeoPointFieldTypeProcessor(object): 3 | """Renders point with tags""" 4 | 5 | name = 'geopoint' 6 | description = 'geo point' 7 | multivalue = True 8 | 9 | def extract(self, value): 10 | return value 11 | 12 | def adapt(self, value, htmlpage=None): 13 | return value 14 | 15 | -------------------------------------------------------------------------------- /slybot/slybot/fieldtypes/price.py: -------------------------------------------------------------------------------- 1 | """ 2 | Price field types 3 | """ 4 | from scrapely import extractors 5 | 6 | class PriceTypeProcessor(object): 7 | """Extracts price from text""" 8 | name = "price" 9 | description = "extracts a price decimal number in the text passed" 10 | 11 | def extract(self, htmlregion): 12 | return extractors.contains_any_numbers(htmlregion.text_content) 13 | 14 | def adapt(self, text, htmlpage=None): 15 | return extractors.extract_price(text) 16 | 17 | -------------------------------------------------------------------------------- /slybot/slybot/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/slybot/slybot/plugins/__init__.py -------------------------------------------------------------------------------- /slybot/slybot/plugins/scrapely_annotations/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .annotations import Annotations 4 | 5 | __all__ = [Annotations] 6 | -------------------------------------------------------------------------------- /slybot/slybot/plugins/scrapely_annotations/exceptions.py: -------------------------------------------------------------------------------- 1 | class MissingRequiredError(Exception): 2 | pass 3 | 4 | 5 | class ItemNotValidError(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /slybot/slybot/plugins/scrapely_annotations/extraction/__init__.py: -------------------------------------------------------------------------------- 1 | from .container_extractors import ( 2 | BaseContainerExtractor, ContainerExtractor, RepeatedContainerExtractor, 3 | RepeatedFieldsExtractor 4 | ) 5 | from .extractors import SlybotIBLExtractor, TemplatePageMultiItemExtractor 6 | from .pageparsing import ( 7 | parse_template, SlybotTemplatePage, SlybotTemplatePageParser 8 | ) 9 | from .region_extractors import BaseExtractor, SlybotRecordExtractor 10 | -------------------------------------------------------------------------------- /slybot/slybot/starturls/feed_generator.py: -------------------------------------------------------------------------------- 1 | import re 2 | from scrapy import Request 3 | _NEWLINE_RE = re.compile('[\r\n]') 4 | 5 | 6 | class FeedGenerator(object): 7 | def __init__(self, callback): 8 | self.callback = callback 9 | 10 | def __call__(self, url): 11 | return Request(url, callback=self.parse_urls) 12 | 13 | def parse_urls(self, response): 14 | newline_urls = _NEWLINE_RE.split(response.text) 15 | urls = [url for url in newline_urls if url] 16 | for url in urls: 17 | yield Request(url, callback=self.callback) 18 | -------------------------------------------------------------------------------- /slybot/slybot/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/slybot/slybot/tests/__init__.py -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/extractors.json: -------------------------------------------------------------------------------- 1 | { 2 | "4fad3762688f920d76000000": { 3 | "regular_expression": "(\\d+)" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "SampleProject" 4 | } 5 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/allowed_domains.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://www.ebay.com/sch/ebayadvsearch/?rt=nc" 5 | ], 6 | "allowed_domains": [ 7 | "www.ebay.com", 8 | "www.yahoo.com" 9 | ], 10 | "exclude_patterns": [], 11 | "respect_nofollow": true, 12 | "follow_patterns": [], 13 | "links_to_follow": "none" 14 | } 15 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/any_allowed_domains.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://www.ebay.com/" 5 | ], 6 | "allowed_domains": null, 7 | "exclude_patterns": [], 8 | "respect_nofollow": true, 9 | "follow_patterns": [], 10 | "scrapes": "default", 11 | "links_to_follow": "none" 12 | } 13 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/books.toscrape.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_patterns": [], 3 | "follow_patterns": [ 4 | "page-\\d+\\.html" 5 | ], 6 | "id": "f8ad-40cc-9916", 7 | "init_requests": [], 8 | "js_disable_patterns": [], 9 | "js_enable_patterns": [], 10 | "js_enabled": false, 11 | "links_to_follow": "patterns", 12 | "page_actions": [], 13 | "respect_nofollow": false, 14 | "start_urls": [ 15 | "http://books.toscrape.com/" 16 | ], 17 | "version": "0.13.0b32" 18 | } 19 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/books.toscrape.com/3652-4fa1-a912.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/books.toscrape.com_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_patterns": [], 3 | "follow_patterns": [], 4 | "id": "f8ad-40cc-9916", 5 | "init_requests": [], 6 | "js_disable_patterns": [], 7 | "js_enable_patterns": [], 8 | "js_enabled": false, 9 | "allowed_domains": null, 10 | "links_to_follow": "all", 11 | "page_actions": [], 12 | "respect_nofollow": false, 13 | "start_urls": [ 14 | "http://books.toscrape.com/" 15 | ], 16 | "version": "0.13.0b34" 17 | } 18 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/cargurus.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [ 3 | ], 4 | "start_urls": [ 5 | "http://www.cargurus.com/Cars/sitemap.html" 6 | ], 7 | "exclude_patterns": [ 8 | "-Pictures-", 9 | "-Specs-", 10 | "-Price-", 11 | "_v", 12 | "-Videos-" 13 | ], 14 | "follow_patterns": [ 15 | "-Overview-", 16 | "-Reviews-", 17 | "/rss/" 18 | ], 19 | "links_to_follow": "patterns", 20 | "respect_nofollow": false 21 | } 22 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/ebay.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://www.ebay.com/sch/ebayadvsearch/?rt=nc" 5 | ], 6 | "init_requests": [ 7 | { 8 | "type": "form", 9 | "form_url": "http://www.ebay.com/sch/ebayadvsearch/?rt=nc", 10 | "xpath": "//form[@name='adv_search_from']", 11 | "fields": [ 12 | { 13 | "xpath": ".//*[@name='_nkw']", 14 | "type": "constants", 15 | "value": ["Cars"] 16 | }, 17 | { 18 | "xpath": ".//*[@name='_in_kw']", 19 | "type": "iterate" 20 | } 21 | ] 22 | } 23 | ], 24 | "exclude_patterns": [], 25 | "respect_nofollow": true, 26 | "follow_patterns": [], 27 | "links_to_follow": "none" 28 | } 29 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/ebay3.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://www.ebay.com/sch/ebayadvsearch/?rt=nc" 5 | ], 6 | "init_requests": [ 7 | { 8 | "type": "form", 9 | "form_url": "http://www.ebay.com/sch/ebayadvsearch/?rt=nc", 10 | "xpath": "//form[@name='adv_search_from']", 11 | "fields": [ 12 | { 13 | "xpath": ".//*[@name='_nkw']", 14 | "type": "constants", 15 | "value": ["{search_string}"] 16 | }, 17 | { 18 | "xpath": ".//*[@name='_in_kw']", 19 | "type": "iterate" 20 | } 21 | ] 22 | } 23 | ], 24 | "exclude_patterns": [], 25 | "respect_nofollow": true, 26 | "follow_patterns": [], 27 | "scrapes": "default", 28 | "links_to_follow": "none" 29 | } 30 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/ebay4.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://www.ebay.com/sch/ebayadvsearch/?rt=nc" 5 | ], 6 | "init_requests": [ 7 | { 8 | "type": "form", 9 | "form_url": "http://www.ebay.com/sch/ebayadvsearch/?rt=nc", 10 | "xpath": "//form[@name='adv_search_from']", 11 | "fields": [ 12 | { 13 | "xpath": ".//*[@name='_nkw']", 14 | "type": "constants", 15 | "value": ["{search_string}"] 16 | }, 17 | { 18 | "xpath": ".//*[@name='_in_kw']", 19 | "type": "iterate" 20 | } 21 | ] 22 | } 23 | ], 24 | "exclude_patterns": [], 25 | "respect_nofollow": true, 26 | "follow_patterns": [], 27 | "scrapes": "default", 28 | "links_to_follow": "none" 29 | } 30 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/example.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": ["http://www.example.com/index.html"], 4 | "init_requests": [ 5 | { 6 | "type": "start", 7 | "url": "http://www.example.com/products.csv", 8 | "link_extractor": { 9 | "type": "column", 10 | "value": 1, 11 | "delimiter": "," 12 | } 13 | } 14 | ], 15 | "exclude_patterns": [], 16 | "follow_patterns": [], 17 | "links_to_follow": "patterns", 18 | "respect_nofollow": true 19 | } 20 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/example2.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [], 4 | "init_requests": [ 5 | { 6 | "type": "start", 7 | "url": "http://www.example.com/index.html" 8 | } 9 | ], 10 | "exclude_patterns": [], 11 | "follow_patterns": [], 12 | "links_to_follow": "patterns", 13 | "respect_nofollow": true 14 | } 15 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/example3.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": ["http://www.example.com/index.html"], 4 | "exclude_patterns": [], 5 | "follow_patterns": [], 6 | "links_to_follow": "patterns", 7 | "respect_nofollow": true 8 | } 9 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/example4.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": ["http://www.example.com/index.html"], 4 | "start_urls_type": "generated_urls", 5 | "generated_urls": [{ 6 | "template": "http://www.example.com/{}", 7 | "paths": [{ 8 | "type": "options", 9 | "values": ["about_us", "contact"] 10 | }], 11 | "params": [] 12 | }, { 13 | "template": "http://www.example.com/{}/{}", 14 | "paths": [{ 15 | "type": "default", 16 | "values": ["p"] 17 | }, { 18 | "type": "range", 19 | "values": [2, 5] 20 | }], 21 | "params": [] 22 | }], 23 | "exclude_patterns": [], 24 | "follow_patterns": [], 25 | "links_to_follow": "patterns", 26 | "respect_nofollow": true 27 | } 28 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/networkhealth.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_names": [ 3 | "networkhealthtemplate", 4 | "doesnotexist" 5 | ], 6 | "start_urls": [ 7 | "http://www.networkhealth.com/network-health-plan-info/all/find-a-doctor/index.aspx" 8 | ], 9 | "exclude_patterns": [], 10 | "follow_patterns": [ 11 | "provider-detail.aspx\\?Id=" 12 | ], 13 | "links_to_follow": "patterns", 14 | "respect_nofollow": true 15 | } 16 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/networkhealth.com/networkhealthtemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extractors": {}, 3 | "url": "http://www.networkhealth.com/network-health-plan-info/all/find-a-doctor/provider-search-provider-detail.aspx?Id=P00138746&Network=HMO/POS", 4 | "scrapes": "doctor", 5 | "page_type": "item", 6 | "page_id": "50b67886d559307f097be904" 7 | } 8 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/pinterest.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://pinterest.com/popular/" 5 | ], 6 | "init_requests": [ 7 | { 8 | "username": "test", 9 | "loginurl": "https://pinterest.com/login/", 10 | "password": "testpass", 11 | "type": "login" 12 | } 13 | ], 14 | "exclude_patterns": [], 15 | "respect_nofollow": true, 16 | "follow_patterns": [], 17 | "links_to_follow": "patterns" 18 | } 19 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/seedsofchange.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [], 3 | "start_urls": [ 4 | "http://www.seedsofchange.com/garden_center/browse_category.aspx?id=123" 5 | ], 6 | "exclude_patterns": [ 7 | "/tellafriend.aspx.+" 8 | ], 9 | "follow_patterns": [ 10 | "/garden_center/browse_category.aspx.+", 11 | "/garden_center/detailedCategoryDisplay.aspx.+", 12 | "/garden_center/product_details.aspx.+" 13 | ], 14 | "links_to_follow": "patterns", 15 | "respect_nofollow": true 16 | } 17 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/SampleProject/spiders/sitemaps.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [ 3 | ], 4 | "start_urls": [ 5 | ], 6 | "exclude_patterns": [ 7 | ], 8 | "follow_patterns": [ 9 | ], 10 | "allowed_domains": [ 11 | "webupd8.org", 12 | "siliconrepublic.com" 13 | ], 14 | "links_to_follow": "patterns", 15 | "respect_nofollow": false 16 | } 17 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/atom_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Webupd8 Posts 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/sitemap_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://www.siliconrepublic.com/post-sitemap1.xml 6 | 2003-06-04T10:46:42+01:00 7 | 8 | 9 | https://www.siliconrepublic.com/post-sitemap2.xml 10 | 2004-02-11T12:29:08+00:00 11 | 12 | 13 | https://www.siliconrepublic.com/post-sitemap3.xml 14 | 2004-09-10T09:48:13+01:00 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /slybot/slybot/tests/data/test_params.txt: -------------------------------------------------------------------------------- 1 | Cars 2 | Boats -------------------------------------------------------------------------------- /slybot/slybot/validation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/slybot/slybot/validation/__init__.py -------------------------------------------------------------------------------- /slybot/tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27,py37 8 | 9 | [testenv] 10 | deps = 11 | -r{toxinidir}/requirements-test.txt 12 | -r{toxinidir}/requirements.txt 13 | commands = 14 | nosetests \ 15 | --with-doctest \ 16 | --with-doctest-ignore-unicode \ 17 | --doctest-options='+IGNORE_UNICODE' 18 | 19 | -------------------------------------------------------------------------------- /slyd/.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | *.py[cod] 3 | 4 | # editor files 5 | *.orig 6 | *.bak 7 | *.swp 8 | *.project 9 | *.sublime-* 10 | 11 | # twisted 12 | dropin.cache 13 | twistd.log 14 | twistd.pid 15 | _trial_temp* 16 | 17 | # local data files 18 | data/* 19 | 20 | # npm files 21 | node_modules/* 22 | npm-debug.log 23 | -------------------------------------------------------------------------------- /slyd/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "$", 6 | "URI", 7 | "CanvasLoader", 8 | "TreeMirror", 9 | "Raven", 10 | "-Promise" 11 | ], 12 | "browser": true, 13 | "boss": false, 14 | "curly": true, 15 | "debug": false, 16 | "devel": true, 17 | "eqeqeq": true, 18 | "evil": true, 19 | "forin": false, 20 | "immed": false, 21 | "laxbreak": false, 22 | "newcap": true, 23 | "noarg": true, 24 | "noempty": false, 25 | "nonew": false, 26 | "nomen": false, 27 | "onevar": false, 28 | "plusplus": false, 29 | "regexp": false, 30 | "undef": true, 31 | "sub": true, 32 | "strict": false, 33 | "white": false, 34 | "eqnull": true, 35 | "esnext": true, 36 | "unused": true 37 | } 38 | -------------------------------------------------------------------------------- /slyd/bin/init_mysql_db: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | try: 7 | import slyd 8 | import slyd.settings as settings_module 9 | from scrapy.settings import Settings 10 | settings = Settings() 11 | settings.setmodule(settings_module) 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings['DJANGO_SETTINGS']) 13 | except ImportError: 14 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 15 | try: 16 | import slyd 17 | except ImportError: 18 | sys.stderr.write("Error: Can't find the project package 'slyd'.\n") 19 | sys.exit(1) 20 | 21 | from portia_server.storage.repoman import Repoman 22 | 23 | 24 | def main(): 25 | Repoman.setup( 26 | storage_backend='slyd.gitstorage.repo.MysqlRepo', 27 | location=os.environ.get('DB_URL'), 28 | ) 29 | Repoman.pool._runWithConnection(Repoman.init_backend) 30 | 31 | 32 | if __name__ == '__main__': 33 | main() 34 | -------------------------------------------------------------------------------- /slyd/bin/slyd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import splash.server 4 | import splash.defaults 5 | import argparse 6 | 7 | 8 | DEFAULT_PORTIA_PORT = 9001 9 | DEFAULT_PORTIA_ROOT = '../portiaui/dist' 10 | splash.defaults.SPLASH_PORT = DEFAULT_PORTIA_PORT 11 | 12 | def parse_args(): 13 | op = argparse.ArgumentParser() 14 | op.add_argument('-p', '--port', default=DEFAULT_PORTIA_PORT, type=int) 15 | op.add_argument('-r', '--root', default=DEFAULT_PORTIA_ROOT, 16 | help='Location of Portia webserver assets') 17 | return op.parse_args() 18 | 19 | 20 | def make_server(*args, **kwargs): 21 | from slyd.tap import makeService 22 | from twisted.internet import reactor 23 | opts = parse_args() 24 | reactor.listenTCP(opts.port, makeService({'port': opts.port, 25 | 'docroot': opts.root})) 26 | 27 | if __name__ == '__main__': 28 | splash.server.main(server_factory=make_server, argv=[]) 29 | -------------------------------------------------------------------------------- /slyd/requirements.txt: -------------------------------------------------------------------------------- 1 | # Slyd requirements 2 | twisted==19.2.1 3 | pyOpenSSL==17.5.0 4 | service_identity==18.1.0 5 | requests>=2.20.0 6 | autobahn==18.3.1 7 | six==1.12.0 8 | chardet==3.0.4 9 | parse==1.8.2 10 | ndg-httpsclient==0.4.4 11 | retrying==1.3.3 12 | mock==2.0.0 13 | 14 | # Splash dev 15 | https://github.com/scrapinghub/splash/archive/3.2.x.tar.gz 16 | -------------------------------------------------------------------------------- /slyd/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | install_requires = ['Scrapy', 'scrapely', 'loginform', 'lxml', 'jsonschema', 4 | 'django', 'parse', 'marshmallow_jsonapi', 'chardet', 5 | 'autobahn', 'requests', 'service_identity', 6 | 'ndg-httpsclient'] 7 | tests_requires = install_requires 8 | 9 | setup(name='slyd', 10 | license='BSD', 11 | description='Portia', 12 | author='Scrapinghub', 13 | url='http://github.com/scrapinghub/portia', 14 | packages=find_packages(), 15 | platforms=['Any'], 16 | scripts=['bin/sh2sly', 'bin/slyd', 'bin/init_mysql_db'], 17 | classifiers=[ 18 | 'Development Status :: 4 - Beta', 19 | 'License :: OSI Approved :: BSD License', 20 | 'Operating System :: OS Independent', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 2', 23 | 'Programming Language :: Python :: 2.7' 24 | ]) 25 | -------------------------------------------------------------------------------- /slyd/slybot: -------------------------------------------------------------------------------- 1 | ../slybot -------------------------------------------------------------------------------- /slyd/slyd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/slyd/slyd/__init__.py -------------------------------------------------------------------------------- /slyd/slyd/authmanager.py: -------------------------------------------------------------------------------- 1 | from scrapy.utils.misc import load_object 2 | 3 | 4 | class AuthManager(object): 5 | 6 | def __init__(self, settings): 7 | self.settings = settings 8 | auth_settings = settings.get('AUTH_CONFIG', {}) 9 | self.auth_method = load_object( 10 | auth_settings.get('CALLABLE', 'slyd.dummyauth.protectResource')) 11 | self.config = auth_settings.get('CONFIG', {}) 12 | 13 | def protectResource(self, resource): 14 | return self.auth_method(resource, config=self.config) 15 | -------------------------------------------------------------------------------- /slyd/slyd/dummyauth.py: -------------------------------------------------------------------------------- 1 | from twisted.web.resource import Resource 2 | 3 | 4 | def protectResource(resource, config): 5 | '''Dummy resource protector.''' 6 | return DummyAuthResource(resource) 7 | 8 | 9 | class DummyAuthResource(Resource): 10 | """A wrapper that injects dummy auth info to every passing request.""" 11 | 12 | def __init__(self, resource): 13 | Resource.__init__(self) 14 | self.wrapped = resource 15 | 16 | def getChildWithDefault(self, path, request): 17 | request.auth_info = { 18 | 'username': 'defaultuser', 19 | } 20 | # Don't consume any segments. 21 | request.postpath.insert(0, request.prepath.pop()) 22 | return self.wrapped 23 | -------------------------------------------------------------------------------- /slyd/slyd/errors.py: -------------------------------------------------------------------------------- 1 | class BaseError(Exception): 2 | def __init__(self, status, title, body=''): 3 | self._status = status 4 | self._title = title 5 | self._body = body 6 | 7 | @property 8 | def title(self): 9 | return self._title 10 | 11 | @property 12 | def body(self): 13 | return self._body 14 | 15 | @property 16 | def status(self): 17 | return self._status 18 | 19 | def __repr__(self): 20 | return '%s(%s)' % (self.__class__.__name__, str(self)) 21 | 22 | def __str__(self): 23 | return '%s: %s' % (self.status, self.title) 24 | 25 | 26 | class BaseHTTPError(BaseError): 27 | _status = 999 28 | 29 | def __init__(self, title, body=''): 30 | super(BaseHTTPError, self).__init__(self._status, title, body) 31 | 32 | 33 | class BadRequest(BaseHTTPError): 34 | _status = 400 35 | -------------------------------------------------------------------------------- /slyd/slyd/gitstorage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/slyd/slyd/gitstorage/__init__.py -------------------------------------------------------------------------------- /slyd/slyd/gitstorage/projectspec.py: -------------------------------------------------------------------------------- 1 | from slyd.projectspec import ProjectSpec 2 | from slyd.gitstorage.projects import GitProjectMixin 3 | 4 | 5 | class GitProjectSpec(GitProjectMixin, ProjectSpec): 6 | def _schedule_data(self, spider, args): 7 | branch = args.pop('branch', [None])[0] 8 | commit = args.pop('commit_id', [None])[0] 9 | project = self.project_name 10 | arg = {} 11 | if commit: 12 | arg['commit'] = commit 13 | elif branch: 14 | arg['branch'] = branch 15 | if not arg and self.storage.repo.has_branch(self.user): 16 | arg['branch'] = self.user 17 | self._checkout_commit_or_head(project, **arg) 18 | commit_id = self.storage._commit.id 19 | return { 20 | 'project': self.project_name, 21 | 'version': commit_id, 22 | 'spider': spider 23 | } 24 | -------------------------------------------------------------------------------- /slyd/slyd/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .base import * 3 | 4 | try: 5 | from local_settings import * 6 | except ImportError: 7 | pass 8 | -------------------------------------------------------------------------------- /slyd/slyd/splash/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrapinghub/portia/606467d278eab2236afcb3d260cb03bf6fb906a0/slyd/slyd/splash/__init__.py -------------------------------------------------------------------------------- /slyd/slyd/splash/qtutils.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from PyQt5.QtCore import QObject 4 | from PyQt5.QtCore import pyqtSlot 5 | from PyQt5.QtWebKit import QWebElement 6 | from PyQt5.QtNetwork import QNetworkRequest 7 | except ImportError: 8 | from PyQt4.QtCore import QObject 9 | from PyQt4.QtCore import pyqtSlot 10 | from PyQt4.QtWebKit import QWebElement 11 | from PyQt4.QtNetwork import QNetworkRequest 12 | 13 | def to_py(obj): 14 | if hasattr(obj, 'toPyObject'): 15 | return obj.toPyObject() 16 | return obj 17 | 18 | -------------------------------------------------------------------------------- /slyd/twisted/plugins/slyd_plugin.py: -------------------------------------------------------------------------------- 1 | """Registers 'twistd slyd' command.""" 2 | from twisted.application.service import ServiceMaker 3 | 4 | finger = ServiceMaker( 5 | 'slyd', 'slyd.tap', 'A server for creating scrapely spiders', 'slyd') 6 | -------------------------------------------------------------------------------- /splash_utils/compile_slybot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | { 4 | echo ";(function(){" 5 | 6 | cat '../portiaui/bower_components/es5-shim/es5-shim.js' 7 | 8 | # Page actions scripts 9 | cat 'waitAsync.js' 10 | cat 'perform_actions.js' 11 | 12 | echo '})();' 13 | } > ../slybot/slybot/splash-script-combined.js 14 | 15 | --------------------------------------------------------------------------------