From fdabc6a32b6c0295d174e2477876c947e0ce02b9 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:33:46 +0000 Subject: [PATCH 1/20] Migrate to webpack 2+ --- diff --git a/hubs/static/client/.babelrc b/hubs/static/client/.babelrc index aef231b..25043d0 100644 --- a/hubs/static/client/.babelrc +++ b/hubs/static/client/.babelrc @@ -1,5 +1,5 @@ { - "presets" : ["es2015", "react", "stage-1"], - "plugins": ["transform-class-properties"] + "presets" : ["es2015", "react"], + "plugins": ["syntax-dynamic-import"] } diff --git a/hubs/static/client/.eslintrc.json b/hubs/static/client/.eslintrc.json index 434bab5..86fbafe 100644 --- a/hubs/static/client/.eslintrc.json +++ b/hubs/static/client/.eslintrc.json @@ -1,4 +1,8 @@ { + "parser": "babel-eslint", + "plugins": [ + "react" + ], "rules": { "arrow-body-style": "off", "func-names": "off", diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json index 504071d..553d1c1 100644 --- a/hubs/static/client/package.json +++ b/hubs/static/client/package.json @@ -1,36 +1,39 @@ { "name": "fedora-hubs", - "version": "0.0.0", + "version": "0.1.0", "description": "Fedora Hubs will provide a communication and collaboration center for Fedora contributors of all types. The idea is that contributors will be able to visit Hubs to check on their involvements across Fedora, discover new places that they can contribute, and more.", "main": "webpack.config.js", "directories": { "doc": "docs" }, "dependencies": { - "prop-types": "^15.5.8", - "react": "^15.4.0", - "react-autosuggest": "~6.1.0", - "react-dom": "^15.4.0", - "react-intl": "~2.1.5", - "react-linkify": "^0.1.3", - "react-timeago": "^3.1.1", + "prop-types": "^15.5.10", + "react": "^15.6.1", + "react-autosuggest": "^9.3.2", + "react-dom": "^15.6.1", + "react-intl": "^2.3.0", + "react-linkify": "^0.2.1", + "react-loadable": "^4.0.4", + "react-timeago": "^3.4.3", "reconnecting-eventsource": "^1.0.1" }, "devDependencies": { - "babel-loader": "~6.2.4", - "babel-plugin-transform-class-properties": "^6.10.2", - "babel-preset-es2015": "~6.9.0", - "babel-preset-react": "~6.5.0", - "babel-jest": "^14.1.0", - "babel-preset-es2015": "^6.9.0", - "babel-preset-jest": "^14.1.0", - "babel-preset-react": "^6.5.0", - "babel-preset-stage-1": "^6.5.0", - "eslint": "^2.13.1", - "jest": "^14.1.0", - "react-addons-test-utils": "^15.4.0", - "react-test-renderer": "^15.4.0", - "webpack": "~1.13.1" + "babel-core": "^6.26.0", + "babel-eslint": "^7.2.3", + "babel-jest": "^20.0.3", + "babel-loader": "^7.1.1", + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-react": "^6.24.1", + "css-loader": "^0.28.4", + "eslint": "^4.4.1", + "eslint-loader": "^1.9.0", + "eslint-plugin-react": "^7.2.1", + "file-loader": "^0.11.2", + "jest": "^20.0.4", + "react-test-renderer": "^15.6.1", + "style-loader": "^0.18.2", + "webpack": "^3.5.5" }, "scripts": { "dev": "webpack --watch", diff --git a/hubs/static/client/webpack.config.js b/hubs/static/client/webpack.config.js index 532e978..efbe75b 100644 --- a/hubs/static/client/webpack.config.js +++ b/hubs/static/client/webpack.config.js @@ -23,14 +23,41 @@ const config = { 'react-dom': 'ReactDOM', }, module : { - loaders : [{ + rules: [ + { + test: /\.jsx?$/, + enforce: 'pre', + loader: 'eslint-loader', + options: { + emitWarning: true, + }, + include : PATHS.app + }, + { test : /\.jsx?$/, - loader : 'babel', - exclude: /(node_modules|bowercomponents)/, + loader : 'babel-loader', + options: { + cacheDirectory: true, + }, + exclude: /node_modules/, include : PATHS.app - }] + }, + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader' + ] + }, + { + test: /\.(png|svg|jpg|gif)$/, + use: [ 'file-loader' ] + } + ] }, - plugins: [ new webpack.optimize.CommonsChunkPlugin("common.js") ] + plugins: [ + new webpack.optimize.CommonsChunkPlugin("common") + ] }; module.exports = config; From a22209d3c3ecfb9df4013a1906e6fc9fcd150d1d Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:33:46 +0000 Subject: [PATCH 2/20] Reformat spaces --- diff --git a/hubs/static/client/webpack.config.js b/hubs/static/client/webpack.config.js index efbe75b..c308851 100644 --- a/hubs/static/client/webpack.config.js +++ b/hubs/static/client/webpack.config.js @@ -2,62 +2,62 @@ const webpack = require('webpack'); const path = require('path'); const PATHS = { - app: path.join(__dirname, 'app'), - build: path.join(__dirname, '..', 'js', 'build') + app: path.join(__dirname, 'app'), + build: path.join(__dirname, '..', 'js', 'build') }; const config = { - entry: { - Hubs: path.join(PATHS.app, 'core', 'Hubs.js'), - Feed: path.join(PATHS.app, 'widgets', 'feed', 'Feed.js'), - Halp: path.join(PATHS.app, 'widgets', 'halp', 'Halp.js') + entry: { + Hubs: path.join(PATHS.app, 'core', 'Hubs.js'), + Feed: path.join(PATHS.app, 'widgets', 'feed', 'Feed.js'), + Halp: path.join(PATHS.app, 'widgets', 'halp', 'Halp.js') + }, + output: { + path: PATHS.build, + filename: '[name].js', + library: '[name]', + }, + externals: { + 'jquery': 'jQuery', + 'react': 'React', + 'react-dom': 'ReactDOM', + }, + module: { + rules: [ + { + test: /\.jsx?$/, + enforce: 'pre', + loader: 'eslint-loader', + options: { + emitWarning: true, + }, + include : PATHS.app }, - output: { - path: PATHS.build, - filename: '[name].js', - library: '[name]', + { + test : /\.jsx?$/, + loader : 'babel-loader', + options: { + cacheDirectory: true, + }, + exclude: /node_modules/, + include : PATHS.app }, - externals: { - 'jquery': 'jQuery', - 'react': 'React', - 'react-dom': 'ReactDOM', + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader' + ] }, - module : { - rules: [ - { - test: /\.jsx?$/, - enforce: 'pre', - loader: 'eslint-loader', - options: { - emitWarning: true, - }, - include : PATHS.app - }, - { - test : /\.jsx?$/, - loader : 'babel-loader', - options: { - cacheDirectory: true, - }, - exclude: /node_modules/, - include : PATHS.app - }, - { - test: /\.css$/, - use: [ - 'style-loader', - 'css-loader' - ] - }, - { - test: /\.(png|svg|jpg|gif)$/, - use: [ 'file-loader' ] - } - ] - }, - plugins: [ - new webpack.optimize.CommonsChunkPlugin("common") + { + test: /\.(png|svg|jpg|gif)$/, + use: [ 'file-loader' ] + } ] + }, + plugins: [ + new webpack.optimize.CommonsChunkPlugin("common") + ] }; module.exports = config; From a79f9cd88ada2554b6a8e0c8b52fd156e8d89319 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:08 +0000 Subject: [PATCH 3/20] Make auth and user change more robust --- diff --git a/hubs/app.py b/hubs/app.py index 8974ae4..56173d1 100644 --- a/hubs/app.py +++ b/hubs/app.py @@ -71,8 +71,8 @@ def check_auth(): # request. user_sub = OIDC.user_getfield("sub") if ("auth" not in flask.session - or not flask.session["auth"] - or flask.session["auth"]["sub"] != user_sub): + or not flask.session["auth"] + or flask.session["auth"].get("sub") != user_sub): user_info = OIDC.user_getinfo([ "name", "nickname", "sub", "email", "zoneinfo", "cla", "groups", diff --git a/hubs/tests/test_authn.py b/hubs/tests/test_authn.py index 350edf3..1ff17c1 100644 --- a/hubs/tests/test_authn.py +++ b/hubs/tests/test_authn.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals import flask import hubs.app -from hubs.tests import APPTest, FakeAuthorization +from hubs.tests import APPTest class AuthnTestCase(APPTest): @@ -23,7 +23,6 @@ class AuthnTestCase(APPTest): def test_expired_auth(self): # Previous user is still in the session - flask.session["auth"] = dict( nickname="olduser", name="olduser", From ddbc205ba1704675024ad2503d01f5d8c7c4205e Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:08 +0000 Subject: [PATCH 4/20] Make auth testing closer to real auth --- diff --git a/hubs/tests/__init__.py b/hubs/tests/__init__.py index 15051a6..fddf404 100644 --- a/hubs/tests/__init__.py +++ b/hubs/tests/__init__.py @@ -141,7 +141,7 @@ def auth_set(APP, auth): g.auth = munch.Munch(logged_in=False) g.user = None else: - g.user = auth.user + g.user = FakeUser(auth.nickname) if flask._app_ctx_stack.top is None: # App context isn't pushed yet @@ -162,25 +162,19 @@ class FakeUser(object): supposed to be. """ self.username = username - self.booksmarks = [] + self.bookmarks = [] self.groups = [] -class FakeAuthorization(object): +def FakeAuthorization(username): """ Fake Authorization used to set as flask.g.auth. """ - - def __init__(self, username='username'): - """ Constructor. - :arg groups: list of the groups in which this fake user is - supposed to be. - """ - self.logged_in = True - self.fullname = 'fullname: ' + username - self.email = 'email: ' + username - self.user = FakeUser(username) - self.avatar = 'avatar_src_url' - self.nickname = username - self.username = username + return munch.Munch( + logged_in=True, + fullname='fullname: ' + username, + nickname=username, + email='email: ' + username, + avatar='avatar_src_url', + ) @contextmanager diff --git a/hubs/tests/test_authz.py b/hubs/tests/test_authz.py index 7a7c85a..f47b85f 100644 --- a/hubs/tests/test_authz.py +++ b/hubs/tests/test_authz.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from hubs.app import app from hubs.authz import ObjectAuthzMixin, AccessLevel, PERMISSIONS -from hubs.tests import APPTest, FakeAuthorization +from hubs.tests import APPTest, FakeUser class TestObj(ObjectAuthzMixin): @@ -35,25 +35,25 @@ class AuthzTestCase(APPTest): def test_mixin_access_level_logged_in(self): obj = TestObj() - user = FakeAuthorization('ralph') + user = FakeUser('ralph') self.assertEqual( obj._get_auth_access_level(user), AccessLevel.logged_in) def test_mixin_group_access_level(self): obj = TestObj(roles={"auth-group": "member"}) - user = FakeAuthorization('ralph') + user = FakeUser('ralph') self.assertEqual( obj._get_auth_access_level(user), AccessLevel.member) def test_mixin_group_access_level_no_role(self): obj = TestObj(roles={"unrelated-group": "member"}) - user = FakeAuthorization('ralph') + user = FakeUser('ralph') self.assertEqual( obj._get_auth_access_level(user), AccessLevel.logged_in) def test_mixin_group_access_level_unsupported(self): obj = TestObj(roles={"auth-group": "dark_lord"}) - user = FakeAuthorization('ralph') + user = FakeUser('ralph') self.assertEqual( obj._get_auth_access_level(user), AccessLevel.logged_in) @@ -68,7 +68,7 @@ class AuthzTestCase(APPTest): self.assertFalse(obj.allows(None, 'widget.restricted.view')) def test_allows_logged_in(self): - user = FakeAuthorization('ralph') + user = FakeUser('ralph') obj = TestObj() self.assertTrue(obj.allows(user, 'hub.public.view')) self.assertTrue(obj.allows(user, 'hub.preview.view')) @@ -79,7 +79,7 @@ class AuthzTestCase(APPTest): self.assertTrue(obj.allows(user, 'widget.restricted.view')) def test_allows_member(self): - user = FakeAuthorization('ralph') + user = FakeUser('ralph') obj = TestObj(roles={"auth-group": "member"}) self.assertTrue(obj.allows(user, 'hub.public.view')) self.assertTrue(obj.allows(user, 'hub.preview.view')) @@ -90,7 +90,7 @@ class AuthzTestCase(APPTest): self.assertTrue(obj.allows(user, 'widget.restricted.view')) def test_allows_sponsor(self): - user = FakeAuthorization('ralph') + user = FakeUser('ralph') obj = TestObj(roles={"auth-group": "sponsor"}) self.assertTrue(obj.allows(user, 'hub.public.view')) self.assertTrue(obj.allows(user, 'hub.preview.view')) @@ -101,7 +101,7 @@ class AuthzTestCase(APPTest): self.assertTrue(obj.allows(user, 'widget.restricted.view')) def test_allows_owner(self): - user = FakeAuthorization('ralph') + user = FakeUser('ralph') obj = TestObj(roles={"auth-group": "owner"}) self.assertTrue(obj.allows(user, 'hub.public.view')) self.assertTrue(obj.allows(user, 'hub.preview.view')) @@ -112,7 +112,7 @@ class AuthzTestCase(APPTest): self.assertTrue(obj.allows(user, 'widget.restricted.view')) def test_allows_site_admin(self): - user = FakeAuthorization('admin') + user = FakeUser('admin') obj = TestObj() for perm in PERMISSIONS.keys(): self.assertTrue(obj.allows(user, perm)) diff --git a/hubs/tests/views/test_user.py b/hubs/tests/views/test_user.py index 127fd36..eb1ed6a 100644 --- a/hubs/tests/views/test_user.py +++ b/hubs/tests/views/test_user.py @@ -50,7 +50,7 @@ class TestGetNotifications(hubs.tests.APPTest): def test_get_notifications_valid_name(self): with hubs.tests.auth_set(app, self.user): resp = self.app.get('/stream/saved/'.format( - self.user.username)) + self.user.nickname)) self.assertEqual(resp.status_code, 200) data = json.loads(resp.get_data(as_text=True)) @@ -63,7 +63,7 @@ class TestGetNotifications(hubs.tests.APPTest): class TestPostNotifications(hubs.tests.APPTest): user = hubs.tests.FakeAuthorization('ralph') valid_payload = { - 'username': user.username, + 'username': user.nickname, 'markup': 'foobar', 'link': 'baz', 'secondary_icon': 'http://placekitten.com/g/200/300', @@ -71,7 +71,7 @@ class TestPostNotifications(hubs.tests.APPTest): } invalid_payload = { - 'username': user.username, + 'username': user.nickname, } def test_post_notification_invalid_payload(self): @@ -99,7 +99,7 @@ class TestPostNotifications(hubs.tests.APPTest): self.assertEqual(notification['link'], 'baz') all_saved = hubs.models.SavedNotification.by_username( - self.user.username) + self.user.nickname) self.assertEqual(len(all_saved), 2) all_saved = [s.__json__() for s in all_saved] self.assertTrue(any(str(s['markup']) == self.valid_payload['markup'] @@ -140,7 +140,7 @@ class TestDeleteNotifications(hubs.tests.APPTest): with hubs.tests.auth_set(app, self.user): resp = self.app.delete( - '/stream/saved/{}/'.format(self.user.username, idx) + '/stream/saved/{}/'.format(self.user.nickname, idx) ) self.assertEqual(resp.status_code, 404) From f563d019796d8b536d1be0acfd31a47e8c6a232c Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:08 +0000 Subject: [PATCH 5/20] Use functions for widget parameter validators --- diff --git a/hubs/tests/test_widget_validators.py b/hubs/tests/test_widget_validators.py index ba0c2b8..d421aa5 100644 --- a/hubs/tests/test_widget_validators.py +++ b/hubs/tests/test_widget_validators.py @@ -10,50 +10,50 @@ from hubs.tests import APPTest class ValidatorsTest(APPTest): def test_required(self): - self.assertRaises(ValueError, validators.Required.from_string, "") + self.assertRaises(ValueError, validators.Required, "") def test_text(self): - self.assertEqual(validators.Text.from_string("\xe9"), "\xe9") + self.assertEqual(validators.Text("\xe9"), "\xe9") def test_integer(self): - self.assertEqual(validators.Integer.from_string("1"), 1) - self.assertRaises(ValueError, validators.Integer.from_string, "text") + self.assertEqual(validators.Integer("1"), 1) + self.assertRaises(ValueError, validators.Integer, "text") @unittest.skip("Not implemented yet") def test_link(self): value = 'dummy' self.assertEqual(validators.Link.from_string(value), value) - self.assertRaises(ValueError, validators.Link.from_string, "text") + self.assertRaises(ValueError, validators.Link, "text") def test_username(self): - self.assertEqual(validators.Username.from_string("ralph"), "ralph") + self.assertEqual(validators.Username("ralph"), "ralph") self.assertRaises( - ValueError, validators.Username.from_string, "nobody") + ValueError, validators.Username, "nobody") @unittest.skip("Not implemented yet") def test_github_organization(self): self.assertEqual( - validators.GithubOrganization.from_string("fedora-infra"), + validators.GithubOrganization("fedora-infra"), "fedora-infra") self.assertRaises( ValueError, - validators.GithubOrganization.from_string, + validators.GithubOrganization, "something-that-does-not-exist") @unittest.skip("Not implemented yet") def test_github_repo(self): self.assertEqual( - validators.GithubRepo.from_string("fedmsg"), "fedmsg") - self.assertRaises(ValueError, validators.GithubRepo.from_string, + validators.GithubRepo("fedmsg"), "fedmsg") + self.assertRaises(ValueError, validators.GithubRepo, "something-that-does-not-exist") def test_fmncontext(self): - self.assertEqual(validators.FMNContext.from_string("email"), "email") + self.assertEqual(validators.FMNContext("email"), "email") self.assertRaises( - ValueError, validators.FMNContext.from_string, "dummy") + ValueError, validators.FMNContext, "dummy") def test_pagure_repo(self): self.assertEqual( - validators.PagureRepo.from_string("fedora-hubs"), "fedora-hubs") - self.assertRaises(ValueError, validators.PagureRepo.from_string, + validators.PagureRepo("fedora-hubs"), "fedora-hubs") + self.assertRaises(ValueError, validators.PagureRepo, "something-that-does-not-exist") diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index 7f35b37..b562662 100644 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -9,7 +9,6 @@ import six from importlib import import_module from .caching import CachedFunction from .view import WidgetView -from .validators import Validator log = logging.getLogger(__name__) @@ -25,18 +24,17 @@ class WidgetParameter(object): :py:meth:`~Widget.get_parameters` method. The value of the parameter is stored in the database as the value returned - by the validator's :py:meth:`from_string` method. It can thus be a string, - an integer, a list, a dict, or any JSON-serializable value. + by the validator's call. It can thus be a string, an integer, a list, a + dict, or any JSON-serializable value. Attributes: name (str): The name of the parameter. label (str): A humanized name of the parameter, which will be shown in the UI. default: The default value if this parameter is not set. - validator (hubs.widgets.validators.Validator): A validator subclass - that will be used to convert the parameter value to and from - string, raising an exception if it is invalid. This attribute - points to the validator subclass, not an instance of the class. + validator (callable): A validator function that will be used to convert + the parameter value to a JSON-serializable value, raising an + exception if it is invalid. help (str): A help text that will be shown in the UI. """ @@ -49,7 +47,7 @@ class WidgetParameter(object): raise TypeError("Invalid attribute: %s" % name) # Set default validator if self.validator is None: - self.validator = Validator + self.validator = lambda x: x class Widget(object): diff --git a/hubs/widgets/validators.py b/hubs/widgets/validators.py index bf05a53..f8c5b8f 100644 --- a/hubs/widgets/validators.py +++ b/hubs/widgets/validators.py @@ -1,82 +1,50 @@ +""" +Validate and convert the value of widget parameters. + +Validators are used to validate and convert +:py:class:`~hubs.widgets.base.WidgetParameter` values. They will raise a +``ValueError`` exception if the value is invalid. + +A validator is a function that will receive the value as unique argument, and +will return the validated value. +""" + from __future__ import unicode_literals import flask import hubs.models import kitchen.text.converters import requests +import six -class Validator(object): - """Convert widget parameters to and from string, and validate their value. - - Validators are used to convert - :py:class:`~hubs.widgets.base.WidgetParameter` values to and from string. - They will raise an exception if the value is invalid. - - A validator is a subclass of the :py:class:`Validator` class and implements - two class methods: :py:meth:`from_string` and :py:meth:`to_string`. - """ - - @classmethod - def from_string(cls, value): - """Convert the value from a string to a JSON-serializable value. - - The result of this function will be stored in the database for widget - parameters. - - Raises: - ValueError: The value is invalid. - """ - if value is None: - return "" - return value - - @classmethod - def to_string(cls, value): - """Convert the value to a string. - - The result of this function will be used in the widget configuration - form fields. - """ - return value - - -class Required(Validator): +def Required(value): """Raises an error if the value is ``False``-like.""" - - @classmethod - def from_string(cls, value): - if not bool(value): - raise ValueError("the parameter is required") - return value + if not bool(value): + raise ValueError("the parameter is required") + # if callable(value): + # # Act as a decorator + # return lambda v: Required(value(v)) + return value -class Text(Validator): +def Text(value): """Raises an error if the value can't be converted to unicode.""" + return kitchen.text.converters.to_unicode(value) - @classmethod - def from_string(cls, value): - return kitchen.text.converters.to_unicode(value) - -class Integer(Validator): +def Integer(value): """Raises an error if the value can't be converted to an integer.""" - - @classmethod - def from_string(cls, value): - return int(value) + return int(value) -class Link(Validator): +def Link(value): """Raises an error if the value doesn't look like a link.""" - - @classmethod - def from_string(cls, value): - # TODO -- verify that this is actually a link - return value + # TODO -- verify that this is actually a link + return value -class Username(Validator): +def Username(value): """Raises an error if the value isn't an existing username. There must be a corresponding :py:class:`~hubs.models.User` record. @@ -84,77 +52,55 @@ class Username(Validator): This validator does not return the User instance because it is not JSON-serializable, it returns the username unchanged. """ - - @classmethod - def from_string(cls, value): - if hubs.models.User.by_username(value) is not None: - return value - raise ValueError('Invalid username') - - @classmethod - def to_string(cls, value): - if value is None and flask.g.auth.logged_in: - return flask.g.user.username + if value is None and flask.g.auth.logged_in: + return flask.g.user.username + if hubs.models.User.by_username(value) is not None: return value + raise ValueError('Invalid username') -class GithubOrganization(Validator): +def GithubOrganization(value): """Fails if the Github organization name does not exist.""" - - @classmethod - def from_string(cls, value): - # TODO -- implement this. - return value + # TODO -- implement this. + return value -class GithubRepo(Validator): +def GithubRepo(value): """Fails if the Github repository name does not exist.""" - - @classmethod - def from_string(cls, value): - # TODO -- implement this. - return value + # TODO -- implement this. + return value -class FMNContext(Validator): +def FMNContext(value): """Fails if the value is not a valid FMN context name.""" - - @classmethod - def from_string(cls, value): - # TODO get this from the fedmsg config. - if value in ['irc', 'email', 'android', 'desktop', 'hubs']: - return value - raise ValueError('Invalid FMN context') + # TODO get this from the fedmsg config. + if value in ['irc', 'email', 'android', 'desktop', 'hubs']: + return value + raise ValueError('Invalid FMN context') -class PagureRepo(Validator): +def PagureRepo(value): """Fails if the Pagure repository name does not exist.""" - - @classmethod - def from_string(cls, value): - response = requests.get("https://pagure.io/%s" % value, timeout=5) - if response.status_code == 200: - return value - raise ValueError('Invalid pagure repo') + response = requests.get("https://pagure.io/%s" % value, timeout=5) + if response.status_code == 200: + return value + raise ValueError('Invalid pagure repo') -class CommaSeparatedList(Validator): - """Fails if the value isn't a comma-separated list. +def CommaSeparatedList(value): + """Fails if the value isn't a list. - The value will be converted to a Python list. If there is no comma in the - original value, it will produce a list with a single element. Whitespaces - will be stripped from the elements, so spaces are allowed around the - commas. + If the value is a string, list, it will be interpreted as a comma-separated + list and converted to a Python list. If there is no comma in the original + value, it will produce a list with a single element. Whitespaces will be + stripped from the elements, so spaces are allowed around the commas. """ - - @classmethod - def from_string(cls, value): + if not value: + return [] + if isinstance(value, six.string_types): return [ elem.strip() for elem in value.split(",") if elem.strip() ] - - @classmethod - def to_string(cls, value): - if not value: - return "" - return ", ".join(value) + if not isinstance(value, list): + raise ValueError("Expected a list") + return value From 107e4fb9f5bcc363d0552b98328983b87fc223e5 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:08 +0000 Subject: [PATCH 6/20] Sort widgets by index by default --- diff --git a/hubs/models.py b/hubs/models.py index eebd178..3b7283d 100644 --- a/hubs/models.py +++ b/hubs/models.py @@ -142,7 +142,8 @@ class Hub(ObjectAuthzMixin, BASE): __tablename__ = 'hubs' name = sa.Column(sa.String(50), primary_key=True) created_on = sa.Column(sa.DateTime, default=datetime.datetime.utcnow) - widgets = relation('Widget', cascade='all,delete', backref='hub') + widgets = relation('Widget', cascade='all,delete', backref='hub', + order_by="Widget.index") config = relation('HubConfig', uselist=False, cascade='all,delete', backref='hub') archived = sa.Column(sa.Boolean, default=False) From cee7c15211ac5929154f9b2e40b5fc616bbdc8cc Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:08 +0000 Subject: [PATCH 7/20] Add the hub's right width as a property with a setter --- diff --git a/hubs/models.py b/hubs/models.py index 3b7283d..498d4fa 100644 --- a/hubs/models.py +++ b/hubs/models.py @@ -337,6 +337,7 @@ class HubConfig(BASE): return { 'summary': self.summary, 'left_width': self.left_width, + 'right_width': self.right_width, 'avatar': self.avatar, 'chat_channel': self.chat_channel, 'chat_domain': self.chat_domain, @@ -347,6 +348,10 @@ class HubConfig(BASE): def right_width(self): return 12 - self.left_width + @right_width.setter + def right_width(self, value): + self.left_width = 12 - value + class SpecificDefaultDict(defaultdict): """A more specific version of defaultdict. From 455ab9a811e66ad6ae2cb46a3fa616584da39a29 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:08 +0000 Subject: [PATCH 8/20] Use a REST API for hub and widget config --- diff --git a/hubs/tests/utils/test_views.py b/hubs/tests/utils/test_views.py index 262df3d..9c4f54f 100644 --- a/hubs/tests/utils/test_views.py +++ b/hubs/tests/utils/test_views.py @@ -5,7 +5,7 @@ import flask import hubs.models from hubs.app import app from hubs.tests import APPTest, FakeAuthorization, auth_set, app_config -from hubs.utils.views import get_visible_widgets, get_sse_url +from hubs.utils.views import get_visible_widgets, get_sse_url, move_widget class ViewUtilsTest(APPTest): @@ -129,3 +129,50 @@ class ViewUtilsTest(APPTest): self.assertEqual( get_sse_url("hub/ralph"), "http://example.com/hub/ralph") + + def test_move_widget(self): + hub = hubs.models.Hub(name="testing") + self.session.add(hub) + widget_names = ["about", "badges", "bugzilla", "contact", "dummy"] + + def get_names(): + return [ + w.plugin for w in + hubs.models.Widget.query.filter_by(hub=hub).order_by(hubs.models.Widget.index) + ] + + def get_widget(name): + return hubs.models.Widget.query.filter_by(hub=hub, plugin=name).one() + + def check_move(name, position, expected): + move_widget(get_widget(name), position) + self.session.flush() + self.session.refresh(hub) + self.assertListEqual(get_names(), expected) + + for index, name in enumerate(widget_names): + widget = hubs.models.Widget( + hub=hub, plugin=name, + left=True, index=index, _config="{}") + self.session.add(widget) + self.assertListEqual(get_names(), widget_names) + # From middle to middle forwards + check_move( + "badges", 3, + ["about", "bugzilla", "contact", "badges", "dummy"]) + # From middle to middle backwards + check_move( + "badges", 1, + ["about", "badges", "bugzilla", "contact", "dummy"]) + # From middle to start + check_move( + "contact", 0, + ["contact", "about", "badges", "bugzilla", "dummy"]) + # From middle to end + check_move( + "badges", 5, + ["contact", "about", "bugzilla", "dummy", "badges"]) + # From start to end + check_move( + "contact", 5, + ["about", "bugzilla", "dummy", "badges", "contact"]) diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidget.test_delete b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidget.test_delete new file mode 100644 index 0000000..1c79340 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidget.test_delete @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n
\n \ + \ \n
\n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31652'] + Content-Type: [text/html; charset=utf-8] + Date: ['Tue, 26 Sep 2017 15:29:31 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiWkRBMk1qSmpOakUzWkdGaU1qRTRNemd5WkdFNFlUWTVOakUwTkRoak9UWXhOR1V5T0dGa05RPT0ifX0.DKwD3A.-Hon7IeCBYGJMVcECsPCVMDRot4; + Expires=Fri, 27-Oct-2017 15:29:32 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidget.test_delete_unauthorized b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidget.test_delete_unauthorized new file mode 100644 index 0000000..002f0d8 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidget.test_delete_unauthorized @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n
\n \ + \ \n
\n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31652'] + Content-Type: [text/html; charset=utf-8] + Date: ['Tue, 26 Sep 2017 15:29:34 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiTnpZelpqTmxZVGhoT1RJMk16aGlabVV4WkRnellUVXdPR00xWmpKbU9EVmtORGcyWVRZeE9BPT0ifX0.DKwD3g.EYEwG8LIbAU2xuYJw0UJyjisahM; + Expires=Fri, 27-Oct-2017 15:29:34 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_removed_widget b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_removed_widget new file mode 100644 index 0000000..3cb4651 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_removed_widget @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n \n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31644'] + Content-Type: [text/html; charset=utf-8] + Date: ['Wed, 27 Sep 2017 12:13:40 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiTkdKbVlUZGhZVFU1TmpNeE4yUTVZMkk0WkdFNFpUWm1aVGcyT0RZNVpERTBaRFEyTURnMVlRPT0ifX0.DK0ndA.2Ge7Vf_BJoy4ILHFnc5mQsTH6QQ; + Expires=Sat, 28-Oct-2017 12:13:40 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets new file mode 100644 index 0000000..3c51168 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n
\n \ + \ \n
\n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31652'] + Content-Type: [text/html; charset=utf-8] + Date: ['Tue, 26 Sep 2017 15:29:35 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiTmprM01qQTJabUZtTVRNd01tTXlORGcyWkRSbE9EQXlZV0poT0Roak1UVmhaalpqTURaaFlRPT0ifX0.DKwD4A.AdJ1dfCC4-moxHJCgtqkdcKir-o; + Expires=Fri, 27-Oct-2017 15:29:36 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets_preview b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets_preview new file mode 100644 index 0000000..6146a59 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets_preview @@ -0,0 +1,898 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n \n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31644'] + Content-Type: [text/html; charset=utf-8] + Date: ['Wed, 27 Sep 2017 12:07:28 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiWldWa1lqZ3lNVFpoT1RBNE5qSmhZemRsTURNeU56TmxNekExTXpnNVlUZGxPV016TlRVMVpnPT0ifX0.DK0mAQ.YtILWDOD3ZiRyLDyFcyzpcGTfaM; + Expires=Sat, 28-Oct-2017 12:07:29 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n \n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31644'] + Content-Type: [text/html; charset=utf-8] + Date: ['Wed, 27 Sep 2017 12:07:31 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiWVdVd01XWTBOVFE0Tnpoak16VXpOV0ZoT1RnME1EUTFNVEV5TUdabVpUUmhaalE1WWpWbFl3PT0ifX0.DK0mAw.6BBoDXAt_1zLVnTcZQa68xAzzTg; + Expires=Sat, 28-Oct-2017 12:07:31 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets_private b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets_private new file mode 100644 index 0000000..0450f91 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_get_widgets_private @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n \n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31644'] + Content-Type: [text/html; charset=utf-8] + Date: ['Wed, 27 Sep 2017 12:07:33 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiT1daa01tWTBOak01WkRCak5ESXlaR1EzTmprNE5qSmpPVGRsTkRSaFlUazJNV0UxWXpZMk5RPT0ifX0.DK0mBQ.zItnVKliuOGIIH4LSzZAPjrw0HY; + Expires=Sat, 28-Oct-2017 12:07:33 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_post_valid_widget_name_no_config b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_post_valid_widget_name_no_config new file mode 100644 index 0000000..cad3a65 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_post_valid_widget_name_no_config @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n
\n \ + \ \n
\n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31652'] + Content-Type: [text/html; charset=utf-8] + Date: ['Tue, 26 Sep 2017 15:29:38 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiWm1Ga09UUTJOR1ExT1RJeVlqRTBNbUV5TlRBMVlqa3lOakZrT0dRNU1tUTBaR000T0dJMk53PT0ifX0.DKwD4g.-AtNMHr2H1oGruvPTfjxzELawsw; + Expires=Fri, 27-Oct-2017 15:29:38 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_private_get b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_private_get new file mode 100644 index 0000000..8119372 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.views.test_api_hub_widget.TestAPIHubWidgets.test_private_get @@ -0,0 +1,450 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.12.5] + method: GET + uri: https://pagure.io/pagure + response: + body: {string: "\n\n\n \n Overview\ + \ - pagure - Pagure\n \n \n \n \n \n \n \n \n\ + \ \n \n
\n
\n
\n \ + \
\n \n \n \n
\n \ + \
\n
\n\n \n\n \ + \
\n
\n
\n
\n
\n\n
\n\n\n
\n
\n
\n \npagure\n \n\ + \
\nA git centered forge  |\ + \  https://pagure.io\ + \
\n\n
\n \n
\n
\n\n
\n \n
\n
\n
\n
\n

Pagure

\n\n\n\ + \n\n\n\n\ + \n
Author:Pierre-Yves Chibon <pingou@pingoured.fr>
\n

Pagure is a git-centered forge, python based using\ + \ pygit2.

\n

With pagure you can host your project with its documentation,\ + \ let your users\nreport issues or request enhancements using the ticketing\ + \ system and build your\ncommunity of contributors by allowing them to fork\ + \ your projects and contribute\nto it via the now-popular pull-request mechanism.

\n\ +

Homepage: https://pagure.io/pagure

\n\ +

See it at work: https://pagure.io

\n\ +

Playground version: https://stg.pagure.io

\n\ +
\n
\n

Get it running

\n

There are several\ + \ options when it comes to a development environment. Vagrant\nwill provide\ + \ you with a virtual machine which you can develop on, or you can\ninstall\ + \ it directly on your host machine.

\n
\n

Vagrant

\n\ +

For a more thorough introduction to Vagrant, see\nhttps://fedoraproject.org/wiki/Vagrant.

\n

An example Vagrantfile\ + \ is provided as Vagrantfile.example. To use it,\njust copy it\ + \ and install Vagrant:

\n
$ cp dev/Vagrantfile.example Vagrantfile\n\
+        $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager\n\
+        $ vagrant up\n
\n

The default Vagrantfile forwards ports\ + \ from the host to the guest,\nso you can interact with the application as\ + \ if it were running on your\nhost machine.

\n
\n

Note

\n\ +

vagrant-hostmanager will automatically\ + \ maintain /etc/hosts for you so you\ncan access the development environment\ + \ from the host using its hostname, which\nby default is pagure-dev.example.com. You can choose not to use this\n\ + functionality by simply not installing the vagrant-hostmanager\ + \ plugin, but\nif you want Pagure to provide valid URLs in the UI for git\ + \ repositories, you\nwill need to adjust Pagure's configuration found in ~/pagure.cfg\ + \ on the guest.

\n
\n
\n
\n

Manually

\n\ +
    \n
  • Install the needed system libraries:

    \n
    sudo dnf install\
    +        \ git python2-virtualenv libgit2-devel \\\n                 libjpeg-devel\
    +        \ gcc libffi-devel redhat-rpm-config\n
    \n
    \n

    Note

    \n\ +

    Do note the version of libgit2 that you install, for example\nin libgit2-0.23.4-1 you need to keep in mind the\ + \ 0.23

    \n
    \n
    \n

    Note

    \n

    On\ + \ Fedora 23 and earlier or on RHEL and derivative (CentOS,\nScientific Linux)\ + \ the package python2-virtualenv is named\npython-virtualenv

    \n\ +
    \n
  • \n
  • Retrieve the sources:

    \n
    git clone https://pagure.io/pagure.git\n\
    +        cd pagure\n
    \n
  • \n
  • Install dependencies

    \n
      \n
    • create\ + \ the virtualenv:

      \n
      virtualenv pagure_env\nsource ./pagure_env/bin/activate\n\
      +        
      \n
    • \n
    • Install the correct version of pygit2:

      \n
      pip\
      +        \ install pygit2==<version of libgit2 found>.*\n
      \n

      So in our\ + \ example:

      \n
      pip install pygit2==0.23.*\n
      \n
    • \n
    • Install\ + \ the rest of the dependencies:

      \n
      pip install -r requirements.txt\n\
      +        
      \n
    • \n
    \n
  • \n
  • Create the folder that will receive the\ + \ projects, forks, docs, requests and\ntickets' git repo:

    \n
    mkdir\
    +        \ -p lcl/{repos,docs,forks,tickets,requests,remotes,attachments,releases}\n\
    +        
    \n
  • \n
  • Create the inital database scheme:

    \n
    python\
    +        \ createdb.py\n
    \n
  • \n
  • Start a worker, in one terminal:

    \n\ +
    ./runworker.py\n
    \n
  • \n
  • Run the application, in another\ + \ terminal:

    \n
    ./runserver.py\n
    \n
  • \n
  • To get some profiling\ + \ information you can also run it as:

    \n
    ./runserver.py --profile\n\
    +        
    \n
  • \n
\n

This will launch the application at http://127.0.0.1:5000

\n
    \n
  • To run unit-tests on pagure

    \n\ +
      \n
    • Install the dependencies:

      \n
      pip install -r tests_requirements.txt\n\
      +        
      \n
    • \n
    • Run it:

      \n
      ./runtests.sh\n
      \n
      \n

      Note

      \n

      While testing for worker tasks, pagure uses celery\ + \ in /usr/bin/\nCelery then looks for eventlet (which we use for testing only)\ + \ at\nsystem level and not in virtualenv. You will need to install eventlet\n\ + outside of your virtualenv if you are using one.

      \n
      \n
    • \n
    \n\ +
  • \n
\n
\n
\n
\n\n
\n
\n\ + \
\n
\n \n
Contributors
\n\ + \
\n
\n \n \n Pierre-YvesChibon (pingou)\n \n \ + \ - main admin\n
\n
\n\ + \ \n \n Ryan Lerch (ryanlerch)\n \n\ + \ - admin\n
\n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n \ + \ \n
\n\ + \
Branches
\n
\n
\n
\n \ + \ \n master\n\ + \
\n
\n \n
\n\ + \
\n\n
\n
\n \ + \ \n \ + \ 2.9.x\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ alembic_changes\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ confirm_merge\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ css_tables\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_del_project\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_delete\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_diff_refs\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_doc\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n\ + \
\n \n \ + \ fix_linking\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_pygit_error\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_reply_to\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_sse_status\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fix_templates\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ fixes\n \n\ + \n
\n
\n
\n
\n
\n \ + \
\n \n force_flag\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ logging_milter\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ mirroring\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n \ + \
\n \n \ + \ py3_work2\n \n\n\ + \
\n
\n
\n
\n
\n \ + \
\n \n rel_2.2.z\n \n\n \ + \
\n
\n \ + \
\n
\n
\n \n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ scroll_metadata\n\ + \ \n\n
\n
\n
\n
\n \ + \
\n
\n \ + \ \n \ + \ utf_search\n \ + \ \n\n
\n
\n
\n
\n \ + \
\n
Source GIT URLs \ + \ more
\n \ + \
\n
\n \ + \
\n
GIT
\n \n \ + \
\n
\n
\n
Docs GIT URLs
\n\ + \
\n
\n
GIT
\n \n \ + \
\n
\n
\n
\n\ + \
\n
\n created 2 years ago\n
\n
\n\n\ + \
\n
\n\ + \ Recent Commits in\n \n \n \ + \ master\n \n \n\ + \
\n
\n \ + \ \n
\n
\n
\n \ + \
\n \n\n\n\n \n\n
\n
\n

\n Copyright © 2014-2017 Red Hat\n \ + \ pagure —\n 3.7.1\ + \ — Documentation\n\ + \

\n

SSH Hostkey/Fingerprint

\n\ + \
\n
\n\n \n \n \n \n \n\n\n\n\ + \n\n\n\n"} + headers: + Connection: [Keep-Alive] + Content-Length: ['31652'] + Content-Type: [text/html; charset=utf-8] + Date: ['Tue, 26 Sep 2017 15:29:39 GMT'] + Keep-Alive: ['timeout=5, max=100'] + Server: [Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 + Python/2.7.5] + Set-Cookie: ['pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmIjp7IiBiIjoiWVRBNU16ZGpNV0ptTlRkaU1qRm1OR0V3WlRjMk1UWXhOREUwTUdGa05qWXhPVEUwTjJRNU13PT0ifX0.DKwD5A.sIltPbKIPmLrrToF0bppHY_o6Cs; + Expires=Fri, 27-Oct-2017 15:29:40 GMT; Secure; HttpOnly; Path=/'] + Strict-Transport-Security: [max-age=15768000; includeSubDomains; preload] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/views/test_api.py b/hubs/tests/views/test_api.py deleted file mode 100644 index abf1e3d..0000000 --- a/hubs/tests/views/test_api.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import unicode_literals - -from six.moves.urllib.parse import urlparse - -import hubs.tests -import hubs.models -from hubs.app import app - - -def usernames(collection): return [u.username for u in collection] - - -class TestHubSubscribe(hubs.tests.APPTest): - user = hubs.tests.FakeAuthorization('decause') - - def test_subscribe_redirects_when_logged_out(self): - hub = hubs.models.Hub.by_name('infra') - resp = self.app.post('/api/hub/{}/subscribe'.format(hub.name), - follow_redirects=False) - self.assertEqual(resp.status_code, 302) - self.assertEqual(urlparse(resp.location).path, '/login') - - def test_subscribe_when_logged_in(self): - hub = hubs.models.Hub.by_name('infra') - with hubs.tests.auth_set(app, self.user): - resp = self.app.post('/api/hub/{}/subscribe'.format(hub.name), - follow_redirects=True) - self.assertEqual(resp.status_code, 200) - # Need to find the Hub again to avoid DetachedInstanceError - h = hubs.models.Hub.by_name('infra') - self.assertTrue(self.user.username in usernames(h.subscribers)) - - -class TestHubUnsubscribe(hubs.tests.APPTest): - user = hubs.tests.FakeAuthorization('decause') - - def test_unsubscribe_redirects_when_logged_out(self): - hub = hubs.models.Hub.by_name('infra') - resp = self.app.post('/api/hub/{}/unsubscribe'.format(hub.name), - follow_redirects=False) - self.assertEqual(resp.status_code, 302) - self.assertEqual(urlparse(resp.location).path, '/login') - - def test_unsubscribe_when_logged_in(self): - hub = hubs.models.Hub.by_name('infra') - # Need a real user model to subscribe to the hub - User = hubs.models.User.by_username(self.user.username) - hub.subscribe(User) - - self.assertTrue(self.user.username in usernames(hub.subscribers)) - - with hubs.tests.auth_set(app, self.user): - resp = self.app.post('/api/hub/{}/unsubscribe'.format(hub.name), - follow_redirects=True) - self.assertEqual(resp.status_code, 200) - h = hubs.models.Hub.by_name('infra') - self.assertTrue(self.user.username not in usernames(h.subscribers)) - - -class TestHubStar(hubs.tests.APPTest): - user = hubs.tests.FakeAuthorization('decause') - - def test_star_redirects_when_logged_out(self): - hub = hubs.models.Hub.by_name('infra') - resp = self.app.post('/api/hub/{}/star'.format(hub.name), - follow_redirects=False) - self.assertEqual(resp.status_code, 302) - self.assertEqual(urlparse(resp.location).path, '/login') - - def test_star_when_logged_in(self): - hub = hubs.models.Hub.by_name('infra') - with hubs.tests.auth_set(app, self.user): - resp = self.app.post('/api/hub/{}/star'.format(hub.name), - follow_redirects=True) - - self.assertEqual(resp.status_code, 200) - h = hubs.models.Hub.by_name('infra') - self.assertTrue(self.user.username in usernames(h.stargazers)) - - -class TestHubUnstar(hubs.tests.APPTest): - user = hubs.tests.FakeAuthorization('decause') - - def test_unstar_redirects_when_logged_out(self): - hub = hubs.models.Hub.by_name('infra') - resp = self.app.post('/api/hub/{}/unstar'.format(hub.name), - follow_redirects=False) - self.assertEqual(resp.status_code, 302) - self.assertEqual(urlparse(resp.location).path, '/login') - - def test_unstar_when_logged_in(self): - hub = hubs.models.Hub.by_name('infra') - # Need a real user model to subscribe to the hub - User = hubs.models.User.by_username(self.user.username) - hub.subscribe(User, role='stargazer') - - self.assertTrue(self.user.username in [u.username for - u in hub.stargazers]) - - with hubs.tests.auth_set(app, self.user): - resp = self.app.post('/api/hub/{}/unstar'.format(hub.name), - follow_redirects=True) - self.assertEqual(resp.status_code, 200) - h = hubs.models.Hub.by_name('infra') - self.assertTrue(self.user.username not in usernames(h.stargazers)) - - -class TestHubJoin(hubs.tests.APPTest): - user = hubs.tests.FakeAuthorization('decause') - - def test_join_redirects_when_logged_out(self): - hub = hubs.models.Hub.by_name('infra') - resp = self.app.post('/api/hub/{}/join'.format(hub.name), - follow_redirects=False) - self.assertEqual(resp.status_code, 302) - self.assertEqual(urlparse(resp.location).path, '/login') - - def test_join_when_logged_in(self): - hub = hubs.models.Hub.by_name('infra') - with hubs.tests.auth_set(app, self.user): - resp = self.app.post('/api/hub/{}/join'.format(hub.name), - follow_redirects=True) - - self.assertEqual(resp.status_code, 200) - h = hubs.models.Hub.by_name('infra') - self.assertTrue(self.user.username in usernames(h.members)) - - -class TestHubLeave(hubs.tests.APPTest): - user = hubs.tests.FakeAuthorization('decause') - - def test_leave_redirects_when_logged_out(self): - hub = hubs.models.Hub.by_name('infra') - resp = self.app.post('/api/hub/{}/leave'.format(hub.name), - follow_redirects=False) - self.assertEqual(resp.status_code, 302) - self.assertEqual(urlparse(resp.location).path, '/login') - - def test_star_when_logged_in(self): - hub = hubs.models.Hub.by_name('infra') - # Need a real user model to subscribe to the hub - User = hubs.models.User.by_username(self.user.username) - hub.subscribe(User, role='member') - - with hubs.tests.auth_set(app, self.user): - resp = self.app.post('/api/hub/{}/leave'.format(hub.name), - follow_redirects=True) - - self.assertEqual(resp.status_code, 200) - h = hubs.models.Hub.by_name('infra') - self.assertTrue(self.user.username not in usernames(h.members)) diff --git a/hubs/tests/views/test_api_association.py b/hubs/tests/views/test_api_association.py new file mode 100644 index 0000000..26eaaa5 --- /dev/null +++ b/hubs/tests/views/test_api_association.py @@ -0,0 +1,151 @@ +from __future__ import unicode_literals + +from six.moves.urllib.parse import urlparse + +import hubs.tests +import hubs.models +from hubs.app import app + + +def usernames(collection): return [u.username for u in collection] + + +class TestHubSubscribe(hubs.tests.APPTest): + user = hubs.tests.FakeAuthorization('decause') + + def test_subscribe_redirects_when_logged_out(self): + hub = hubs.models.Hub.by_name('infra') + resp = self.app.post('/api/hubs/{}/subscribe'.format(hub.name), + follow_redirects=False) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.location).path, '/login') + + def test_subscribe_when_logged_in(self): + hub = hubs.models.Hub.by_name('infra') + with hubs.tests.auth_set(app, self.user): + resp = self.app.post('/api/hubs/{}/subscribe'.format(hub.name), + follow_redirects=True) + self.assertEqual(resp.status_code, 200) + # Need to find the Hub again to avoid DetachedInstanceError + h = hubs.models.Hub.by_name('infra') + self.assertTrue(self.user.nickname in usernames(h.subscribers)) + + +class TestHubUnsubscribe(hubs.tests.APPTest): + user = hubs.tests.FakeAuthorization('decause') + + def test_unsubscribe_redirects_when_logged_out(self): + hub = hubs.models.Hub.by_name('infra') + resp = self.app.post('/api/hubs/{}/unsubscribe'.format(hub.name), + follow_redirects=False) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.location).path, '/login') + + def test_unsubscribe_when_logged_in(self): + hub = hubs.models.Hub.by_name('infra') + # Need a real user model to subscribe to the hub + User = hubs.models.User.by_username(self.user.nickname) + hub.subscribe(User) + + self.assertTrue(self.user.nickname in usernames(hub.subscribers)) + + with hubs.tests.auth_set(app, self.user): + resp = self.app.post('/api/hubs/{}/unsubscribe'.format(hub.name), + follow_redirects=True) + self.assertEqual(resp.status_code, 200) + h = hubs.models.Hub.by_name('infra') + self.assertTrue(self.user.nickname not in usernames(h.subscribers)) + + +class TestHubStar(hubs.tests.APPTest): + user = hubs.tests.FakeAuthorization('decause') + + def test_star_redirects_when_logged_out(self): + hub = hubs.models.Hub.by_name('infra') + resp = self.app.post('/api/hubs/{}/star'.format(hub.name), + follow_redirects=False) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.location).path, '/login') + + def test_star_when_logged_in(self): + hub = hubs.models.Hub.by_name('infra') + with hubs.tests.auth_set(app, self.user): + resp = self.app.post('/api/hubs/{}/star'.format(hub.name), + follow_redirects=True) + + self.assertEqual(resp.status_code, 200) + h = hubs.models.Hub.by_name('infra') + self.assertTrue(self.user.nickname in usernames(h.stargazers)) + + +class TestHubUnstar(hubs.tests.APPTest): + user = hubs.tests.FakeAuthorization('decause') + + def test_unstar_redirects_when_logged_out(self): + hub = hubs.models.Hub.by_name('infra') + resp = self.app.post('/api/hubs/{}/unstar'.format(hub.name), + follow_redirects=False) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.location).path, '/login') + + def test_unstar_when_logged_in(self): + hub = hubs.models.Hub.by_name('infra') + # Need a real user model to subscribe to the hub + User = hubs.models.User.by_username(self.user.nickname) + hub.subscribe(User, role='stargazer') + + self.assertTrue(self.user.nickname in [u.username for + u in hub.stargazers]) + + with hubs.tests.auth_set(app, self.user): + resp = self.app.post('/api/hubs/{}/unstar'.format(hub.name), + follow_redirects=True) + self.assertEqual(resp.status_code, 200) + h = hubs.models.Hub.by_name('infra') + self.assertTrue(self.user.nickname not in usernames(h.stargazers)) + + +class TestHubJoin(hubs.tests.APPTest): + user = hubs.tests.FakeAuthorization('decause') + + def test_join_redirects_when_logged_out(self): + hub = hubs.models.Hub.by_name('infra') + resp = self.app.post('/api/hubs/{}/join'.format(hub.name), + follow_redirects=False) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.location).path, '/login') + + def test_join_when_logged_in(self): + hub = hubs.models.Hub.by_name('infra') + with hubs.tests.auth_set(app, self.user): + resp = self.app.post('/api/hubs/{}/join'.format(hub.name), + follow_redirects=True) + + self.assertEqual(resp.status_code, 200) + h = hubs.models.Hub.by_name('infra') + self.assertTrue(self.user.nickname in usernames(h.members)) + + +class TestHubLeave(hubs.tests.APPTest): + user = hubs.tests.FakeAuthorization('decause') + + def test_leave_redirects_when_logged_out(self): + hub = hubs.models.Hub.by_name('infra') + resp = self.app.post('/api/hubs/{}/leave'.format(hub.name), + follow_redirects=False) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.location).path, '/login') + + def test_star_when_logged_in(self): + hub = hubs.models.Hub.by_name('infra') + # Need a real user model to subscribe to the hub + User = hubs.models.User.by_username(self.user.nickname) + hub.subscribe(User, role='member') + + with hubs.tests.auth_set(app, self.user): + resp = self.app.post('/api/hubs/{}/leave'.format(hub.name), + follow_redirects=True) + + self.assertEqual(resp.status_code, 200) + h = hubs.models.Hub.by_name('infra') + self.assertTrue(self.user.nickname not in usernames(h.members)) diff --git a/hubs/tests/views/test_api_hub_config.py b/hubs/tests/views/test_api_hub_config.py new file mode 100644 index 0000000..b542ce1 --- /dev/null +++ b/hubs/tests/views/test_api_hub_config.py @@ -0,0 +1,185 @@ +from __future__ import unicode_literals + +import datetime + +from flask import json + +from hubs.app import app +from hubs.models import Hub, User +from hubs.tests import APPTest, FakeAuthorization, auth_set + + +class TestAPIHubConfig(APPTest): + + def test_get(self): + hub = Hub.by_name('ralph') + hub.last_refreshed = datetime.datetime(2017, 1, 1, 0, 0, 0) + self.session.commit() + user = FakeAuthorization('ralph') + response = self.check_url("/api/hubs/ralph/config", user=user) + expected_data = { + "user_hub": True, + "config": { + "avatar": "https://seccdn.libravatar.org/avatar/" + "9c9f7784935381befc302fe3c814f9136e7a339" + "53d0318761669b8643f4df55c?s=312&d=retro", + 'chat_channel': None, + 'chat_domain': None, + "left_width": 8, + "right_width": 4, + "summary": "Ralph", + "visibility": "public", + }, + "mtime": "Sun, 01 Jan 2017 00:00:00 GMT", + "users": { + "owner": [ + {"username": "ralph", "fullname": "Ralph"}, + ], + "member": [], + }, + "name": "ralph", + } + response_data = json.loads(response.get_data(as_text=True)) + self.assertDictEqual( + response_data, {"status": "OK", "data": expected_data}) + + def test_private_get(self): + hub = Hub.by_name('ralph') + hub.config.visibility = "private" + self.session.commit() + # Private hubs are not accessible to anonymous users. + response = self.check_url("/api/hubs/ralph/config", code=403) + self.assertDictEqual( + json.loads(response.get_data(as_text=True)), + { + "hub": "ralph", + "message": "This hub is for members only.", + "status": "ERROR" + } + ) + # But they are accessible to members. + decause = User.by_username("decause") + hub.subscribe(decause, role='member') + self.check_url( + "/api/hubs/ralph/config", user=FakeAuthorization("decause")) + + def test_put(self): + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.put( + '/api/hubs/ralph/config', + content_type="application/json", + data=json.dumps({ + "config": { + "summary": "changed value", + "chat_domain": "", + } + }) + ) + self.assertEqual(result.status_code, 200) + result_data = json.loads(result.get_data(as_text=True)) + self.assertEqual(result_data["status"], "OK") + hub_config = Hub.query.get("ralph").config + self.assertEqual(hub_config.summary, "changed value") + self.assertEqual( + hub_config.chat_domain, + app.config["CHAT_NETWORKS"][0]["domain"]) + + def test_put_unknown_data(self): + # Unknown PUT data is silently ignored + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.put( + '/api/hubs/ralph/config', + content_type="application/json", + data=json.dumps({"config": {"non_existant": "dummy"}})) + self.assertEqual(result.status_code, 200) + result_data = json.loads(result.get_data(as_text=True)) + self.assertEqual(result_data["status"], "ERROR") + self.assertEqual(result_data["message"], "Invalid value(s)") + self.assertIn("non_existant", result_data["fields"]) + self.assertEqual( + result_data["fields"]["non_existant"], + "Unexpected parameter." + ) + + def test_put_invalid_chat_domain(self): + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.put( + '/api/hubs/ralph/config', + content_type="application/json", + data=json.dumps({"config": {"chat_domain": "dummy"}})) + self.assertEqual(result.status_code, 200) + result_data = json.loads(result.get_data(as_text=True)) + self.assertEqual(result_data["status"], "ERROR") + self.assertEqual(result_data["message"], "Invalid value(s)") + self.assertIn("chat_domain", result_data["fields"]) + self.assertEqual( + result_data["fields"]["chat_domain"], + "Unsupported chat domain." + ) + + def test_put_unauthorized(self): + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.put( + '/api/hubs/decause/config', + content_type="application/json", + data=json.dumps({"config": {"summary": "Defaced!"}})) + self.assertEqual(result.status_code, 403) + hub_config = Hub.query.get("decause").config + self.assertEqual(hub_config.summary, "Decause") + + def test_suggest_users_no_filter(self): + user = FakeAuthorization('ralph') + expected = [ + dict(username=u.username, fullname=u.fullname) + for u in User.query.order_by(User.username).all() + ] + # Check without filter + response = self.check_url( + "/api/hubs/ralph/config/suggest-users", user=user) + result_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(result_data["status"], "OK") + self.assertListEqual(result_data["data"], expected) + + def test_suggest_users_filter_owners(self): + # Filters on owners + user = FakeAuthorization('ralph') + expected = [ + dict(username=u.username, fullname=u.fullname) + for u in + User.query.order_by(User.username).filter( + User.username != "ralph").all() + ] + response = self.check_url( + "/api/hubs/ralph/config/suggest-users?exclude-role=owner", + user=user) + result_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(result_data["status"], "OK") + self.assertListEqual(result_data["data"], expected) + + def test_suggest_users_filter_members(self): + # Filters on members + user = FakeAuthorization('ralph') + hub = Hub.get('ralph') + decause = User.query.get("decause") + devyani7 = User.query.get("devyani7") + hub.subscribe(decause, "member") + hub.subscribe(devyani7, "member") + expected = [ + dict(username=u.username, fullname=u.fullname) + for u in + User.query.order_by( + User.username + ).filter( + User.username != "decause", + User.username != "devyani7" + ).all()] + response = self.check_url( + "/api/hubs/ralph/config/suggest-users?exclude-role=member", + user=user) + result_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(result_data["status"], "OK") + self.assertListEqual(result_data["data"], expected) diff --git a/hubs/tests/views/test_api_hub_widget.py b/hubs/tests/views/test_api_hub_widget.py new file mode 100644 index 0000000..5684d29 --- /dev/null +++ b/hubs/tests/views/test_api_hub_widget.py @@ -0,0 +1,240 @@ +from __future__ import unicode_literals + +from flask import json + +from hubs.app import app +from hubs.models import Hub, User, Widget +from hubs.widgets import registry +from hubs.tests import APPTest, FakeAuthorization, auth_set + + +class TestAPIHubWidgets(APPTest): + + def test_get_widgets(self): + expected_ids = [35, 36, 34, 37, 38, 39, 40, 41, 42, 43, 44, 56] + response = self.check_url("/api/hubs/ralph/widgets/") + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "OK") + self.assertListEqual( + [w["idx"] for w in response_data["data"]], + expected_ids) + + def test_get_widgets_private(self): + hub = Hub.by_name('ralph') + hub.config.visibility = "private" + self.session.commit() + # Private hubs are not accessible to anonymous users. + response = self.check_url("/api/hubs/ralph/widgets/", code=403) + self.assertDictEqual( + json.loads(response.get_data(as_text=True)), + { + "hub": "ralph", + "message": "This hub is for members only.", + "status": "ERROR" + } + ) + # But they are accessible to members. + decause = User.by_username("decause") + hub.subscribe(decause, role='member') + self.check_url( + "/api/hubs/ralph/widgets/", user=FakeAuthorization("decause")) + + def test_get_widgets_preview(self): + hub = Hub.by_name('ralph') + hub.config.visibility = "preview" + meetings = Widget.query.filter_by( + hub=hub, plugin="meetings").first() + # set the widget to "restricted" + meetings.visibility = "restricted" + # Restricted widgets in preview hubs aren't displayed to + # anonymous users. + response = self.check_url("/api/hubs/ralph/widgets/") + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "OK") + self.assertNotIn( + "meetings", [w["name"] for w in response_data["data"]]) + # But they are displayed to members. + response = self.check_url( + "/api/hubs/ralph/widgets/", user=FakeAuthorization('ralph')) + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "OK") + self.assertIn( + "meetings", [w["name"] for w in response_data["data"]]) + + def test_get_removed_widget(self): + hub = Hub.get("ralph") + widget = Widget( + hub=hub, plugin="does-not-exist", + left=True, index=-1, _config="{}") + self.session.add(widget) + response = self.check_url( + "/api/hubs/ralph/widgets/", user=FakeAuthorization('ralph')) + response_data = json.loads(response.get_data(as_text=True)) + self.assertNotIn( + "does-not-exist", [w["name"] for w in response_data["data"]]) + + def test_get_available(self): + response = self.check_url("/api/widgets/") + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "OK") + self.assertListEqual( + [w["name"] for w in response_data["data"]], + list(registry.keys())) + + def test_post_invalid_request(self): + invalid_data = [ + { + "position": "left", + 'config': {}, + }, { + 'name': 'about', + 'position': 'right', + }, { + 'name': 'about', + 'config': {}, + }, { + 'name': 'about', + 'config': {}, + 'position': 'invalid_position', + }, + ] + user = FakeAuthorization('ralph') + with auth_set(app, user): + for data in invalid_data: + result = self.app.post( + '/api/hubs/ralph/widgets/', + content_type="application/json", + data=json.dumps(data)) + self.assertEqual(result.status_code, 400) + expected_str = 'Invalid request sent' + self.assertIn(expected_str, result.get_data(as_text=True)) + + def test_post_unknown_widget(self): + invalid_data = [ + { + 'name': 'invalid_widget_name', + 'config': {}, + 'position': 'right', + }, + ] + user = FakeAuthorization('ralph') + with auth_set(app, user): + for data in invalid_data: + result = self.app.post( + '/api/hubs/ralph/widgets/', + content_type="application/json", + data=json.dumps(data)) + self.assertEqual(result.status_code, 400) + expected_str = 'Unknown widget' + self.assertIn(expected_str, result.get_data(as_text=True)) + + def test_post_valid_widget_name_no_config(self): + user = FakeAuthorization('ralph') + with auth_set(app, user): + data = { + 'name': 'memberships', + 'position': 'right', + "config": {}, + } + result = self.app.post( + '/api/hubs/ralph/widgets/', + content_type="application/json", + data=json.dumps(data)) + self.assertEqual(result.status_code, 200) + self.assertEqual( + json.loads(result.get_data(as_text=True)), + {"status": "OK"}) + response = self.check_url("/api/hubs/ralph/widgets/") + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "OK") + self.assertIn("memberships", + [w["name"] for w in response_data["data"]]) + + def test_post_valid_widget_name_with_config(self): + self.assertEqual( + Widget.query.filter( + Hub.name == "ralph", + Widget.plugin == "about", + ).count(), 1) + data = { + "name": "about", + "config": {'text': 'text of widget'}, + 'position': 'right', + } + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.post( + '/api/hubs/ralph/widgets/', + content_type="application/json", + data=json.dumps(data)) + self.assertEqual(result.status_code, 200) + self.assertEqual( + json.loads(result.get_data(as_text=True)), + {"status": "OK"}) + self.assertEqual( + Widget.query.filter( + Hub.name == "ralph", + Widget.plugin == "about", + ).count(), 2) + + +class TestAPIHubWidget(APPTest): + + def test_get_logged_in(self): + user = FakeAuthorization('ralph') + response = self.check_url("/api/hubs/ralph/widgets/37/", user=user) + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "OK") + self.assertEqual(response_data["data"]["name"], "fedmsgstats") + + def test_get_logged_out(self): + hub = Hub.by_name('ralph') + hub.config.visibility = "private" + self.session.commit() + response = self.check_url("/api/hubs/ralph/widgets/31/", code=403) + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "ERROR") + + def test_put_empty_data_logged_in(self): + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.put( + '/api/hubs/ralph/widgets/37/', + content_type="application/json", + data=json.dumps({})) + self.assertEqual(result.status_code, 200) + self.assertEqual( + json.loads(result.get_data(as_text=True)), + {"status": "OK"}) + + def test_put_unauthorized(self): + user = FakeAuthorization('decause') + with auth_set(app, user): + result = self.app.put( + '/api/hubs/ralph/widgets/37/', + content_type="application/json", + data=json.dumps({"config": {"text": "Defaced!"}})) + self.assertEqual(result.status_code, 403) + + def test_delete(self): + user = FakeAuthorization('ralph') + with auth_set(app, user): + result = self.app.delete('/api/hubs/ralph/widgets/37/') + self.assertEqual(result.status_code, 200) + self.assertEqual( + json.loads(result.get_data(as_text=True)), + {"status": "OK"}) + response = self.check_url("/api/hubs/ralph/widgets/") + response_data = json.loads(response.get_data(as_text=True)) + self.assertNotIn(37, [w["idx"] for w in response_data["data"]]) + + def test_delete_unauthorized(self): + user = FakeAuthorization('decause') + with auth_set(app, user): + response = self.app.delete('/api/hubs/ralph/widgets/37/') + self.assertEqual(response.status_code, 403) + response_data = json.loads(response.get_data(as_text=True)) + self.assertEqual(response_data["status"], "ERROR") + response = self.check_url("/api/hubs/ralph/widgets/") + response_data = json.loads(response.get_data(as_text=True)) + self.assertIn(37, [w["idx"] for w in response_data["data"]]) diff --git a/hubs/tests/views/test_hub_config.py b/hubs/tests/views/test_hub_config.py deleted file mode 100644 index 9b66b03..0000000 --- a/hubs/tests/views/test_hub_config.py +++ /dev/null @@ -1,166 +0,0 @@ -from __future__ import unicode_literals - -from flask import json - -from hubs.app import app -from hubs.models import Hub, HubConfig, User -from hubs.tests import APPTest, FakeAuthorization, auth_set - - -class HubConfigTestCase(APPTest): - - def test_hub_config_get(self): - expected = { - "hubconfig": { - "summary": "Ralph", - "left_width": 8, - "avatar": ( - "https://seccdn.libravatar.org/avatar/9c9f7784935381befc30" - "2fe3c814f9136e7a33953d0318761669b8643f4df55c" - "?s=312&d=retro" - ), - "chat_channel": None, - "chat_domain": None, - "visibility": "public", - }, - "general": { - "chat_networks": app.config["CHAT_NETWORKS"], - "roles": ["owner", "member"], - "hub_visibility": HubConfig.VISIBILITY, - }, - "users": { - "member": [], - "owner": [{ - "fullname": "Ralph", - "locked": True, - "role": "owner", - "username": "ralph", - }], - }, - } - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/config' - result = self.app.get(url) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "OK") - self.assertDictEqual(result_data["result"], expected) - - def test_hub_config_post(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/config?category=config' - result = self.app.post(url, data={ - "summary": "changed value", - "chat_domain": "", - }) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "OK") - self.assertEqual( - result_data["result"]["hubconfig"]["summary"], - "changed value") - self.assertEqual( - result_data["result"]["hubconfig"]["chat_domain"], - app.config["CHAT_NETWORKS"][0]["domain"]) - - def test_hub_config_post_unknown_post_data(self): - # Unknown POST data is silently ignored - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/config?category=config' - result = self.app.post(url, data={"non_existant": "dummy"}) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "ERROR") - self.assertEqual(result_data["message"], "Invalid value(s)") - self.assertIn("non_existant", result_data["fields"]) - self.assertEqual( - result_data["fields"]["non_existant"], - "Unexpected parameter." - ) - - def test_hub_config_post_invalid_chat_domain(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/config?category=config' - result = self.app.post(url, data={"chat_domain": "dummy"}) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "ERROR") - self.assertEqual(result_data["message"], "Invalid value(s)") - self.assertIn("chat_domain", result_data["fields"]) - self.assertEqual( - result_data["fields"]["chat_domain"], - "Unsupported chat domain." - ) - - def test_hub_config_get_unauthorized(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/decause/config' - result = self.app.get(url) - self.assertEqual(result.status_code, 403) - - def test_hub_config_post_unauthorized(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/decause/config?category=config' - result = self.app.post(url, data={"summary": "Defaced!"}) - self.assertEqual(result.status_code, 403) - - def test_hub_config_suggest_users_no_filter(self): - user = FakeAuthorization('ralph') - expected = [ - u.username for u in - User.query.order_by(User.username).all() - ] - # Check without filter - with auth_set(app, user): - url = '/ralph/config/suggest-users' - result = self.app.get(url) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "OK") - self.assertListEqual(result_data["results"], expected) - - def test_hub_config_suggest_users_filter_owners(self): - # Filters on owners - user = FakeAuthorization('ralph') - expected = [ - u.username for u in - User.query.order_by(User.username).filter( - User.username != "ralph").all() - ] - with auth_set(app, user): - url = '/ralph/config/suggest-users?exclude-role=owner' - result = self.app.get(url) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "OK") - self.assertListEqual(result_data["results"], expected) - - def test_hub_config_suggest_users_filter_members(self): - # Filters on members - user = FakeAuthorization('ralph') - hub = Hub.get('ralph') - decause = User.query.get("decause") - devyani7 = User.query.get("devyani7") - hub.subscribe(decause, "member") - hub.subscribe(devyani7, "member") - expected = [ - u.username for u in - User.query.order_by( - User.username - ).filter( - User.username != "decause", - User.username != "devyani7" - ).all()] - with auth_set(app, user): - url = '/ralph/config/suggest-users?exclude-role=member' - result = self.app.get(url) - self.assertEqual(result.status_code, 200) - result_data = json.loads(result.get_data(as_text=True)) - self.assertEqual(result_data["status"], "OK") - self.assertListEqual(result_data["results"], expected) diff --git a/hubs/tests/views/test_hub_edit.py b/hubs/tests/views/test_hub_edit.py deleted file mode 100644 index 2ff727e..0000000 --- a/hubs/tests/views/test_hub_edit.py +++ /dev/null @@ -1,114 +0,0 @@ -from __future__ import unicode_literals - -from six.moves.urllib.parse import urlparse -from werkzeug.datastructures import ImmutableMultiDict - -from hubs.app import app -from hubs.tests import APPTest, FakeAuthorization, auth_set - - -class HubEditTestCase(APPTest): - - def test_hub_edit_get_logged_out(self): - with auth_set(app, None): - result = self.app.get('/ralph/edit', follow_redirects=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(urlparse(result.location).path, "/login") - - def test_hub_edit_get_logged_in_not_owner(self): - user = FakeAuthorization('not_ralph') - with auth_set(app, user): - result = self.app.get('/ralph/edit', follow_redirects=True) - self.assertEqual(result.status_code, 403) - - def test_hub_edit_get_logged_in_owner(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.get('/ralph/edit', follow_redirects=True) - self.assertEqual(result.status_code, 200) - - def test_hub_edit_post_logged_out(self): - with auth_set(app, None): - result = self.app.post('/ralph/edit', follow_redirects=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(urlparse(result.location).path, "/login") - - def test_hub_edit_post_logged_in_not_owner(self): - user = FakeAuthorization('not_ralph') - with auth_set(app, user): - result = self.app.post('/ralph/edit', follow_redirects=True) - self.assertEqual(result.status_code, 403) - - def test_hub_edit_post_logged_in_owner_empty_data(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.post('/ralph/edit', data={}, - follow_redirects=True) - self.assertEqual(result.status_code, 200) - self.assertNotIn( - 'Not logged in. Click to login', - result.get_data(as_text=True)) - self.assertIn( - 'Full Name: ' - 'fullname: ralph', result.get_data(as_text=True)) - - def test_hub_edit_post_logged_in_owner_valid_data(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - data = ImmutableMultiDict( - [('right_indexes[]', u'0'), ('right_indexes[]', u'1'), - ('right_indexes[]', u'2'), ('right_indexes[]', u'3'), - ('right_indexes[]', u'4'), ('right_indexes[]', u'5'), - ('right_indexes[]', u'6'), ('right_indexes[]', u'7'), - ('right_indexes[]', u'8'), ('right_widgets[]', u'32'), - ('right_widgets[]', u'33'), ('right_widgets[]', u'34'), - ('right_widgets[]', u'35'), ('right_widgets[]', u'36'), - ('right_widgets[]', u'37'), ('right_widgets[]', u'38'), - ('right_widgets[]', u'39'), ('right_widgets[]', u'40'), - ('js', u'true'), ('left_indexes[]', u'0'), - ('left_indexes[]', u'1'), ('left_widgets[]', u'31'), - ('left_widgets[]', u'32')]) - result = self.app.post('/ralph/edit', data=data, - follow_redirects=True) - self.assertEqual(result.status_code, 200) - self.assertEqual(result.get_data(as_text=True), 'ok') - - def test_hub_edit_post_logged_in_owner_invalid_data_1(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - # some indexes and widgets are not integers - data = ImmutableMultiDict( - [('right_indexes[]', u'0a'), ('right_indexes[]', u'1'), - ('right_indexes[]', u'2'), ('right_indexes[]', u'3'), - ('right_indexes[]', u'4'), ('right_indexes[]', u'5'), - ('right_indexes[]', u'6'), ('right_indexes[]', u'7'), - ('right_indexes[]', u'8'), ('right_widgets[]', u'32'), - ('right_widgets[]', u'33a'), ('right_widgets[]', u'34'), - ('right_widgets[]', u'35'), ('right_widgets[]', u'36'), - ('right_widgets[]', u'37'), ('right_widgets[]', u'38'), - ('right_widgets[]', u'39'), ('right_widgets[]', u'40'), - ('js', u'true'), ('left_indexes[]', u'0a'), - ('left_indexes[]', u'1'), ('left_widgets[]', u'31a'), - ('left_widgets[]', u'32')]) - result = self.app.post('/ralph/edit', data=data, - follow_redirects=True) - self.assertEqual(result.status_code, 400) - - def test_hub_edit_post_logged_in_owner_invalid_data_2(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - # indexes len don't match widgets len - data = ImmutableMultiDict( - [('right_indexes[]', u'1'), ('right_indexes[]', u'2'), - ('right_indexes[]', u'3'), ('right_indexes[]', u'4'), - ('right_indexes[]', u'5'), ('right_indexes[]', u'6'), - ('right_indexes[]', u'7'), ('right_indexes[]', u'8'), - ('right_widgets[]', u'32'), ('right_widgets[]', u'34'), - ('right_widgets[]', u'35'), ('right_widgets[]', u'36'), - ('right_widgets[]', u'37'), ('right_widgets[]', u'38'), - ('right_widgets[]', u'39'), ('right_widgets[]', u'40'), - ('js', u'true'), ('left_indexes[]', u'0'), - ('left_indexes[]', u'1'), ('left_widgets[]', u'32')]) - result = self.app.post('/ralph/edit', data=data, - follow_redirects=True) - self.assertEqual(result.status_code, 400) diff --git a/hubs/tests/views/test_hub_widgets.py b/hubs/tests/views/test_hub_widgets.py deleted file mode 100644 index bef72bb..0000000 --- a/hubs/tests/views/test_hub_widgets.py +++ /dev/null @@ -1,138 +0,0 @@ -from __future__ import unicode_literals - -from flask import json -from six.moves.urllib.parse import urlparse - -from hubs.app import app -from hubs.models import Hub, Widget -from hubs.tests import APPTest, FakeAuthorization, auth_set - - -class HubWidgetsTestCase(APPTest): - - def test_hub_add_widget_get_no_args(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.get('/ralph/add', follow_redirects=False) - self.assertEqual(result.status_code, 400) - expected_str = 'Invalid position provided' - self.assertIn(expected_str, result.get_data(as_text=True)) - - def test_hub_add_widget_get_with_args(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.get('/ralph/add?position=right', - follow_redirects=True) - self.assertEqual(result.status_code, 200) - page_html = result.get_data(as_text=True) - self.assertIn('Adding a widget to hub: ralph', page_html) - self.assertIn('
', page_html) - self.assertIn('', - page_html) - - def test_hub_add_widget_post_no_widget_name(self): - data = {"position": "left"} - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.post( - '/ralph/add', data=data, follow_redirects=False) - self.assertEqual(result.status_code, 400) - expected_str = 'Invalid request sent' - self.assertIn(expected_str, result.get_data(as_text=True)) - - def test_hub_add_widget_post_invalid_widget_name(self): - data = {'widget': 'invalid_widget_name', - 'position': 'right'} - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.post( - '/ralph/add', data=data, follow_redirects=False) - self.assertEqual(result.status_code, 404) - expected_str = 'Unknown widget called' - self.assertIn(expected_str, result.get_data(as_text=True)) - - def test_hub_add_widget_post_valid_widget_name_no_args(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - data = { - 'widget': 'memberships', - 'position': 'right', - } - result = self.app.post('/ralph/add', data=data) - self.assertEqual(result.status_code, 200) - self.assertEqual( - json.loads(result.get_data(as_text=True)), - {"status": "ADDED"}) - result = self.app.get('/ralph/edit') - self.assertEqual(result.status_code, 200) - page_html = result.get_data(as_text=True) - self.assertIn('data-url="/ralph/w/memberships/', page_html) - - def test_hub_add_widget_post_valid_widget_name_with_args(self): - self.assertEqual( - Widget.query.filter( - Hub.name == "ralph", - Widget.plugin == "about", - ).count(), 1) - user = FakeAuthorization('ralph') - with auth_set(app, user): - data = { - 'text': 'text of widget', - 'position': 'right', - } - result = self.app.post('/ralph/add/about', data=data, - follow_redirects=False) - self.assertEqual(result.status_code, 200) - self.assertEqual( - json.loads(result.get_data(as_text=True)), - {"status": "ADDED"}) - self.assertEqual( - Widget.query.filter( - Hub.name == "ralph", - Widget.plugin == "about", - ).count(), 2) - - def test_hub_edit_widget_get_logged_in(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - result = self.app.get('/ralph/37/edit', follow_redirects=True) - self.assertEqual(result.status_code, 200) - expected_str = '/ralph/37/edit' - self.assertIn(expected_str, result.get_data(as_text=True)) - - def test_hub_edit_widget_get_logged_out(self): - result = self.app.get('/ralph/31/edit', follow_redirects=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(urlparse(result.location).path, "/login") - - def test_hub_edit_widget_post_empty_data_logged_in(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - data = {} - url = '/ralph/37/edit' - result = self.app.post(url, data=data, follow_redirects=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(urlparse(result.location).path, '/ralph/edit') - - def test_hub_add_widget_valid_side(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/add/about?position=right' - result = self.app.get(url) - self.assertIn('Adding widget "about" to hub ralph', - result.get_data(as_text=True)) - - def test_hub_add_widget_invalid_side(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/add/about?position=invalid' - result = self.app.get(url) - self.assertEqual(result.status_code, 400) - - def test_hub_delete(self): - user = FakeAuthorization('ralph') - with auth_set(app, user): - url = '/ralph/37/delete' # 37 is widget fedmsgstats for ralph - result = self.app.post(url) - self.assertEqual(result.status_code, 302) - self.assertIn('/ralph/edit', result.get_data(as_text=True)) diff --git a/hubs/tests/views/test_widget.py b/hubs/tests/views/test_widget.py deleted file mode 100644 index 7a8d2f7..0000000 --- a/hubs/tests/views/test_widget.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import unicode_literals - - -from hubs.app import app -from hubs.tests import APPTest, auth_set - - -class WidgetTestCase(APPTest): - - def test_source_name(self): - with auth_set(app, None): - url = '/source/about' - result = self.app.get(url) - self.assertEqual(result.status_code, 302) - expected_str = 'https://pagure.io/fedora-hubs/' \ - 'blob/develop/f/hubs/widgets/about/__init__.py' - self.assertIn(expected_str, result.get_data(as_text=True)) - - def test_source_name_not_existent(self): - with auth_set(app, None): - url = '/source/notexistent' - result = self.app.get(url) - self.assertEqual(result.status_code, 404) diff --git a/hubs/tests/widgets/test_about.py b/hubs/tests/widgets/test_about.py index 1cb4ab1..703af25 100644 --- a/hubs/tests/widgets/test_about.py +++ b/hubs/tests/widgets/test_about.py @@ -1,25 +1,10 @@ from __future__ import unicode_literals -import json - -from hubs.tests import FakeAuthorization, widget_instance from . import WidgetTest class TestAbout(WidgetTest): plugin = 'about' # The name in hubs.widgets.registry - def test_data_simple(self): - widget = widget_instance('ralph', self.plugin) - user = FakeAuthorization('ralph') - response = self.check_url('/ralph/%i/json' % widget.idx, user) - data = json.loads(response.get_data(as_text=True)) - self.assertDictEqual(data['data'], { - 'text': 'Testing.', - 'title': " About", - 'edit_mode': False, - }) - def test_view_authz(self): self._test_view_authz() diff --git a/hubs/tests/widgets/test_badges.py b/hubs/tests/widgets/test_badges.py index a984070..9151ed2 100644 --- a/hubs/tests/widgets/test_badges.py +++ b/hubs/tests/widgets/test_badges.py @@ -1,11 +1,9 @@ from __future__ import unicode_literals -import json - from mock import Mock, patch import hubs.widgets -from hubs.tests import FakeAuthorization, widget_instance +from hubs.tests import widget_instance from . import WidgetTest @@ -14,11 +12,9 @@ class TestBadges(WidgetTest): def test_data_simple(self): widget = widget_instance('ralph', self.plugin) - user = FakeAuthorization('ralph') - response = self.check_url('/ralph/%i/json' % widget.idx, user) - data = json.loads(response.get_data(as_text=True)) - self.assertEquals(data['plugin'], 'badges') - self.assertIn('assertions', data['data'].keys()) + func = widget.module.get_cached_functions()['GetBadges'] + result = func(widget)() + self.assertIn('assertions', result.keys()) def test_should_invalidate_wrong_topic(self): widget = widget_instance('ralph', self.plugin) diff --git a/hubs/tests/widgets/test_contact.py b/hubs/tests/widgets/test_contact.py index 7e06862..f011392 100644 --- a/hubs/tests/widgets/test_contact.py +++ b/hubs/tests/widgets/test_contact.py @@ -76,9 +76,10 @@ class ContactsTest(WidgetTest): def test_data_simple(self): user = FakeAuthorization('ralph') - response = self.check_url('/ralph/%i/json' % self.widget_idx, user) - data = json.loads(response.get_data(as_text=True)) - self.assertDictEqual(data['data'], { + widget = Widget.query.get(self.widget_idx) + response = self.check_url( + '/ralph/w/contact/%i/' % self.widget_idx, user) + self.assertDictEqual(response.context, { 'account_age': 'Oct 2010', 'email': 'ralph@fedoraproject.org', 'ircnick': 'ralph', @@ -88,6 +89,8 @@ class ContactsTest(WidgetTest): 'timezone': 'UTC', 'usergroup': True, 'edit_mode': False, + 'widget': widget.module, + 'widget_instance': widget, }) def test_view_authz(self): diff --git a/hubs/tests/widgets/test_fedmsgstats.py b/hubs/tests/widgets/test_fedmsgstats.py index 2a4508a..91778b4 100644 --- a/hubs/tests/widgets/test_fedmsgstats.py +++ b/hubs/tests/widgets/test_fedmsgstats.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import json - from hubs.tests import FakeAuthorization, widget_instance from . import WidgetTest @@ -12,9 +10,9 @@ class TestFedmsgStats(WidgetTest): def test_data_simple(self): widget = widget_instance('ralph', self.plugin) user = FakeAuthorization('ralph') - response = self.check_url('/ralph/%i/json' % widget.idx, user) - data = json.loads(response.get_data(as_text=True)) - self.assertDictEqual(data['data'], { + response = self.check_url( + '/ralph/w/%s/%i/' % (self.plugin, widget.idx), user) + self.assertDictEqual(response.context, { 'fedmsgs': 83854, 'fedmsgs_text': '83,854', 'subscribers': [], @@ -22,9 +20,11 @@ class TestFedmsgStats(WidgetTest): 'subscribers_text': '0', 'subscribed_text': '0', 'username': 'ralph', - 'hub_subscribe_url': '/api/hub/ralph/subscribe', - 'hub_unsubscribe_url': '/api/hub/ralph/unsubscribe', + 'hub_subscribe_url': '/api/hubs/ralph/subscribe', + 'hub_unsubscribe_url': '/api/hubs/ralph/unsubscribe', 'edit_mode': False, + 'widget': widget.module, + 'widget_instance': widget, }) def test_view_authz(self): diff --git a/hubs/tests/widgets/test_halp.py b/hubs/tests/widgets/test_halp.py index 2e33ade..5a489d3 100644 --- a/hubs/tests/widgets/test_halp.py +++ b/hubs/tests/widgets/test_halp.py @@ -117,57 +117,6 @@ class HalpViewsTestCase(WidgetTest): def test_view_authz(self): self._test_view_authz() - def test_json(self): - response = self.app.get('/ralph/%i/json' % self.widget.idx) - self.assertEqual(response.status_code, 200) - data = json.loads(response.get_data(as_text=True)) - self.assertEquals(data['plugin'], 'halp') - self.assertIn('hubs', data['data'].keys()) - - def test_config_add(self): - user = FakeAuthorization('ralph') - response = self.check_url('/ralph/w/halp/add?position=right', user) - self.assertEqual(response.context["mode"], "add") - self.assertEqual(response.context["initial"], { - "hubs": ["ralph"], "per_page": 4, - }) - self.assertEqual( - response.context["url"], "/ralph/add/halp?position=right") - - def test_config_edit(self): - user = FakeAuthorization('ralph') - url = '/ralph/w/halp/%i/config' % self.widget.idx - response = self.check_url(url, user) - self.assertEqual(response.context["mode"], "edit") - self.assertEqual(response.context["initial"], { - 'hubs': ['fedora-devel'], - 'per_page': 3, - }) - self.assertEqual( - response.context["url"], "/ralph/%i/edit" % self.widget.idx) - - def test_config_authz(self): - urls = [ - '/ralph/w/halp/add', - '/ralph/w/halp/%i/config' % self.widget.idx, - ] - # Anonymous - for url in urls: - self.check_url(url, None, 403) - # Logged-in - user = FakeAuthorization('decause') - for url in urls: - self.check_url(url, user, 403) - # Member - hub = Hub.query.get("ralph") - hub.subscribe(User.query.get("decause"), "member") - for url in urls: - self.check_url(url, user, 403) - # Owner - user = FakeAuthorization('ralph') - for url in urls: - self.check_url(url, user, 200) - def test_config_hubs_suggest(self): response = self.app.get('/w/halp/hubs') self.assertEqual(response.status_code, 200) diff --git a/hubs/tests/widgets/test_library.py b/hubs/tests/widgets/test_library.py index 11e4c92..6cb8297 100644 --- a/hubs/tests/widgets/test_library.py +++ b/hubs/tests/widgets/test_library.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import json - from hubs.tests import FakeAuthorization, widget_instance from . import WidgetTest @@ -12,27 +10,17 @@ class TestLibrary(WidgetTest): def test_data_simple(self): widget = widget_instance('ralph', self.plugin) user = FakeAuthorization('ralph') - response = self.check_url('/ralph/%i/json' % widget.idx, user) - data = json.loads(response.get_data(as_text=True)) + response = self.check_url( + '/ralph/w/%s/%i/' % (self.plugin, widget.idx), user) expected_dict = { - "config": { - "urls": "ralph/" - }, - "data": { - 'title': 'Library', - "urls": [ - "ralph/" - ], - 'edit_mode': False, - }, - "description": None, - "hub": "ralph", - "id": 52, - "index": 51, - "left": False, - "plugin": "library" + "urls": [ + "ralph/" + ], + 'edit_mode': False, + 'widget': widget.module, + 'widget_instance': widget, } - self.assertDictEqual(data['data'], expected_dict['data']) + self.assertDictEqual(response.context, expected_dict) def test_view_authz(self): self._test_view_authz() diff --git a/hubs/tests/widgets/test_meetings.py b/hubs/tests/widgets/test_meetings.py index 0ac32a6..6536856 100644 --- a/hubs/tests/widgets/test_meetings.py +++ b/hubs/tests/widgets/test_meetings.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import json - from hubs.tests import FakeAuthorization, widget_instance from . import WidgetTest @@ -13,10 +11,9 @@ class TestMeetings(WidgetTest): team = 'i18n' widget = widget_instance(team, self.plugin) user = FakeAuthorization('ralph') - response = self.check_url('/%s/%i/json' % (team, widget.idx), user) - data = json.loads(response.get_data(as_text=True)) - calendar_name = data['data']['calendar'] - self.assertIn(team, calendar_name) + response = self.check_url( + '/%s/w/%s/%i/' % (team, self.plugin, widget.idx), user) + self.assertIn(team, response.context["calendar"]) def test_render_simple(self): team = 'i18n' diff --git a/hubs/utils/views.py b/hubs/utils/views.py index cc576c7..36df575 100644 --- a/hubs/utils/views.py +++ b/hubs/utils/views.py @@ -74,13 +74,14 @@ class WidgetConfigError(Exception): pass -def create_widget_instance(hub, widget, position): +def create_widget_instance(hub, widget, position, config=None): """View helper to instanciate a widget on a hub. Arguments: hub (hubs.models.Hub): Instance of the hub to create the widget in. widget (hubs.widgets.base.Widget): Widget to instanciate. - position (str): either ``left`` or ``right``. + position (str): Either ``left`` or ``right``. + config (dict): Initial configuration for the widget. """ widget_instance = Widget( hub=hub, plugin=widget.name, index=-1, @@ -88,28 +89,34 @@ def create_widget_instance(hub, widget, position): flask.g.db.add(widget_instance) flask.g.db.flush() # will populate Widget.config try: - configure_widget_instance(widget_instance) + configure_widget_instance(widget_instance, config) except WidgetConfigError as e: flask.flash(e.args[0], "error") else: flask.flash("The widget has been added.") + # reorder widgets + reorder_widgets(hub, widget_instance.left) + return widget_instance -def configure_widget_instance(widget_instance): +def configure_widget_instance(widget_instance, widget_config): """Configure a widget instance using the request form data. Arguments: widget_instance (hubs.models.Widget): Instance of the widget that is to be configured. + widget_config (dict): Configuration to apply to the widget. """ + if not widget_config: + return config = {} for param in widget_instance.module.get_parameters(): - val = flask.request.form.get(param.name) + val = widget_config.get(param.name) if not val: raise WidgetConfigError( 'You must provide a value for: %s' % param.name) try: - config[param.name] = param.validator.from_string(val) + config[param.name] = param.validator(val) except ValueError as err: raise WidgetConfigError('Invalid data provided, error: %s' % err) # Updating in-place is not supported, it's a class property. @@ -119,7 +126,7 @@ def configure_widget_instance(widget_instance): widget_instance.config = cur_config # Set the visibility parameter. try: - widget_instance.visibility = flask.request.form["visibility"] + widget_instance.visibility = widget_config["visibility"] except KeyError: pass # the value is optional. # Update the hub's mtime. TODO: use a SQLAlchemy signal here. @@ -136,6 +143,27 @@ def configure_widget_instance(widget_instance): 'if the error persists, please warn an admin') +def reorder_widgets(hub, is_left): + """Reset widget instances indicies for a hub and a column.""" + all_widgets = [w for w in hub.widgets if w.left == is_left] + for index, widget in enumerate(all_widgets): + widget.index = index + + +def move_widget(widget_instance, new_index): + """Move a widget instance to a new index in the same column.""" + all_widgets = [ + w for w in widget_instance.hub.widgets + if w.left == widget_instance.left + ] + old_index = [w.idx for w in all_widgets].index(widget_instance.idx) + # Move the widget in the list + all_widgets.insert(new_index, all_widgets.pop(old_index)) + # Apply indices according to the position in the list + for index, widget in enumerate(all_widgets): + widget.index = index + + def authenticated(): """ Utility function checking if the current auth is set or not.""" return hasattr(flask.g, 'auth') \ @@ -158,33 +186,43 @@ def login_required(function): return decorated_function +def check_hub_access(hub, action, json=False): + """Check access to the hub for the specified action. + + Raises: + :py:class:`werkzeug.exceptions.Forbidden`: The action is denied (403). + """ + try: + user = flask.g.user + except AttributeError: + user = None + if not hub.allows(user, action): + if action == "view": + msg = "This hub is for members only." + elif action == "config": + msg = "You are not allowed to configure this hub." + else: + msg = "Access forbidden." + if json: + response = flask.jsonify({ + "status": "ERROR", "message": msg, "hub": hub.name, + }) + else: + response = flask.render_template( + 'errors/hub.html', hub=hub, msg=msg, + ) + flask.abort(403, response=flask.make_response(response, 403)) + return True + + def require_hub_access(action, url_param="name", json=False): - """Check access to the hub for the specified action.""" + """View decorator to check access to the hub for the specified action.""" def decorator(function): @functools.wraps(function) def wrapper(*args, **kwargs): hub_name = kwargs[url_param] hub = get_hub(hub_name, load_config=True) - try: - user = flask.g.user - except AttributeError: - user = None - if not hub.allows(user, action): - if action == "view": - msg = "This hub is for members only." - elif action == "config": - msg = "You are not allowed to configure this hub." - else: - msg = "Access forbidden." - if json: - result = flask.jsonify({ - "status": "ERROR", "message": msg, "hub": hub.name, - }) - else: - result = flask.render_template( - 'errors/hub.html', hub=hub, msg=msg, - ) - return result, 403 + check_hub_access(hub, action, json) return function(*args, **kwargs) return wrapper return decorator diff --git a/hubs/views/__init__.py b/hubs/views/__init__.py index 33b68f2..4ea49ec 100644 --- a/hubs/views/__init__.py +++ b/hubs/views/__init__.py @@ -4,6 +4,5 @@ from __future__ import absolute_import from .root import * from .hub import * -from .widget import * from .user import * from .api import * diff --git a/hubs/views/api.py b/hubs/views/api.py deleted file mode 100644 index b1232a5..0000000 --- a/hubs/views/api.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import unicode_literals, absolute_import - - -import flask -import hubs.models - -from hubs.app import app -from hubs.utils.views import get_hub, login_required - - -@app.route('/api/hub//subscribe', methods=['POST']) -@login_required -def hub_subscribe(hub): - hub = get_hub(hub) - user = hubs.models.User.by_username(flask.g.auth.nickname) - hub.subscribe(user) - flask.g.db.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//unsubscribe', methods=['POST']) -@login_required -def hub_unsubscribe(hub): - hub = get_hub(hub) - user = hubs.models.User.by_username(flask.g.auth.nickname) - try: - hub.unsubscribe(user) - except KeyError: - return flask.abort(400) - flask.g.db.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//star', methods=['POST']) -@login_required -def hub_star(hub): - hub = get_hub(hub) - user = hubs.models.User.by_username(flask.g.auth.nickname) - hub.subscribe(user, role='stargazer') - flask.g.db.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//unstar', methods=['POST']) -@login_required -def hub_unstar(hub): - hub = get_hub(hub) - user = hubs.models.User.by_username(flask.g.auth.nickname) - try: - hub.unsubscribe(user, role='stargazer') - except KeyError: - return flask.abort(400) - flask.g.db.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//join', methods=['POST']) -@login_required -def hub_join(hub): - hub = get_hub(hub) - user = hubs.models.User.by_username(flask.g.auth.nickname) - hub.subscribe(user, role='member') - flask.g.db.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//leave', methods=['POST']) -@login_required -def hub_leave(hub): - hub = get_hub(hub) - user = hubs.models.User.by_username(flask.g.auth.nickname) - try: - hub.unsubscribe(user, role='member') - except KeyError: - return flask.abort(400) - flask.g.db.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) diff --git a/hubs/views/api/__init__.py b/hubs/views/api/__init__.py new file mode 100644 index 0000000..5375623 --- /dev/null +++ b/hubs/views/api/__init__.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import + +# flake8: noqa + +from .hub_association import * +from .hub_config import * +from .hub_widget import * diff --git a/hubs/views/api/hub_association.py b/hubs/views/api/hub_association.py new file mode 100644 index 0000000..9b5e8ba --- /dev/null +++ b/hubs/views/api/hub_association.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import + +import logging + +import flask + +import hubs.models +from hubs.app import app +from hubs.utils.views import get_hub, login_required + +log = logging.getLogger(__name__) + + +@app.route('/api/hubs//subscribe', methods=['POST']) +@login_required +def api_hub_subscribe(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.auth.nickname) + hub.subscribe(user) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hubs//unsubscribe', methods=['POST']) +@login_required +def api_hub_unsubscribe(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.auth.nickname) + try: + hub.unsubscribe(user) + except KeyError: + return flask.abort(400) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hubs//star', methods=['POST']) +@login_required +def api_hub_star(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.auth.nickname) + hub.subscribe(user, role='stargazer') + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hubs//unstar', methods=['POST']) +@login_required +def api_hub_unstar(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.auth.nickname) + try: + hub.unsubscribe(user, role='stargazer') + except KeyError: + return flask.abort(400) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hubs//join', methods=['POST']) +@login_required +def api_hub_join(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.auth.nickname) + hub.subscribe(user, role='member') + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hubs//leave', methods=['POST']) +@login_required +def api_hub_leave(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.auth.nickname) + try: + hub.unsubscribe(user, role='member') + except KeyError: + return flask.abort(400) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) diff --git a/hubs/views/api/hub_config.py b/hubs/views/api/hub_config.py new file mode 100644 index 0000000..fa76e29 --- /dev/null +++ b/hubs/views/api/hub_config.py @@ -0,0 +1,144 @@ +from __future__ import absolute_import + +import logging + +import flask + +import hubs.models +from hubs.app import app +from hubs.utils.views import ( + get_hub, check_hub_access, RequestValidator, require_hub_access, + ) + +log = logging.getLogger(__name__) + + +class ConfigChangeError(Exception): + pass + + +@app.route('/api/hubs//config', methods=['GET', 'PUT']) +@require_hub_access("view", json=True) +def api_hub_config(name): + hub = get_hub(name) + if flask.request.method == 'PUT': + check_hub_access(hub, "config", json=True) + request_data = flask.request.get_json() + if request_data is None: + return flask.jsonify({ + "status": "ERROR", + "message": "No configuration change provided" + }) + try: + if request_data.get("config"): + result = hub_config_put_config(hub, request_data["config"]) + if request_data.get("users"): + result = hub_config_put_users(hub, request_data["users"]) + except ConfigChangeError as e: + result = e.args[0] + else: + result = {"status": "OK"} + try: + flask.g.db.commit() + except Exception as err: + result = {"status": "ERROR", "message": str(err)} + return flask.jsonify(result) + result = {"status": "OK", "data": hub.get_props()} + return flask.jsonify(result) + + +def hub_config_put_config(hub, config): + # Validate values + def _validate_chat_domain(value): + valid_chat_domains = [ + network["domain"] for network in app.config["CHAT_NETWORKS"] + ] + if not value and len(valid_chat_domains) > 0: + value = valid_chat_domains[0] + if value not in valid_chat_domains: + raise ValueError("Unsupported chat domain.") + return value + + validator = RequestValidator(dict( + # Only allow the parameters listed in __json__(). + (key, None) for key in hub.config.__json__().keys() + )) + validator.converters["chat_domain"] = _validate_chat_domain + try: + values = validator(config) + except ValueError as e: + result = {"status": "ERROR", "message": "Invalid value(s)"} + result["fields"] = e.args[0] + raise ConfigChangeError(result) + + # Now set the configuration values. + for key, value in values.items(): + setattr(hub.config, key, value) + + +def hub_config_put_users(hub, user_roles): + new_associations = [] + for role in user_roles: + for u in user_roles[role]: + new_associations.append((u["username"], role)) + # Remove old associations + for assoc in hub.associations: + if assoc.hub != hub: + continue + try: + new_associations.remove( + (assoc.user.username, assoc.role)) + except ValueError: + hub.associations.remove(assoc) + # Add new associations + for assoc in new_associations: + username, role = assoc + user = hubs.models.User.get(username=username) + if user is None: + result = { + "status": "ERROR", + "fields": {"username": "Unknown user"}, + "message": "Unknown user {}".format(u["username"]), + } + raise ConfigChangeError(result) + hub.associations.append(hubs.models.Association( + hub=hub, user=user, role=role)) + flask.g.db.commit() + + +@app.route('/api/hubs//config/suggest-users') +@require_hub_access("config", json=True) +def api_hub_config_suggest_users(name): + MAX_SUGGESTS = 10 + hub = get_hub(name) + results = flask.g.db.query( + hubs.models.User.username, + hubs.models.User.fullname, + ) + query = flask.request.args.get("q") + if query: + results = results.filter( + hubs.models.User.username.ilike("%{}%".format(query)) + ) + exclude = flask.request.args.get("exclude-role") + if exclude: + exclude_query = flask.g.db.query( + hubs.models.User.username + ).join( + hubs.models.Association + ).join( + hubs.models.Hub + ).filter( + hubs.models.Hub.name == hub.name, + hubs.models.Association.role == exclude, + ) + results = results.filter( + ~hubs.models.User.username.in_(exclude_query) + ) + results = results.order_by(hubs.models.User.username).limit(MAX_SUGGESTS) + return flask.jsonify({ + "status": "OK", + "data": [ + {"username": r[0], "fullname": r[1]} for r in results + ], + }) diff --git a/hubs/views/api/hub_widget.py b/hubs/views/api/hub_widget.py new file mode 100644 index 0000000..3ef777d --- /dev/null +++ b/hubs/views/api/hub_widget.py @@ -0,0 +1,111 @@ +from __future__ import absolute_import + +import logging + +import flask +from hubs.app import app +from hubs.widgets import registry +from hubs.utils.views import ( + create_widget_instance, configure_widget_instance, get_hub, + get_widget_instance, WidgetConfigError, + check_hub_access, require_hub_access, + move_widget, reorder_widgets, + ) + +log = logging.getLogger(__name__) + + +@app.route('/api/widgets/', methods=['GET']) +def api_widgets(): + widgets = [] + for widget in registry.values(): + widgets.append(widget.get_props(None)) + return flask.jsonify({"status": "OK", "data": widgets}) + + +@app.route('/api/hubs//widgets/', methods=['GET', 'POST']) +@require_hub_access("view", url_param="hub", json=True) +def api_hub_widgets(hub): + hub = get_hub(hub) + try: + user = flask.g.user + except AttributeError: + user = None + if flask.request.method == 'POST': + check_hub_access(hub, "config", json=True) + request_data = flask.request.get_json() + try: + widget_name = request_data["name"] + widget_config = request_data["config"] + position = request_data["position"] + except KeyError: + flask.abort(400, 'Invalid request sent') + if position not in ["left", "right"]: + flask.abort(400, 'Invalid request sent') + available_widgets = [ + widget.name + for widget in registry.values() + if widget.position in ['both', position] + ] + if widget_name not in available_widgets: + flask.abort(400, 'Unknown widget') + widget = registry[widget_name] + create_widget_instance(hub, widget, position, widget_config) + flask.g.db.commit() + return flask.jsonify({"status": "OK"}) + user_can_config = hub.allows(user, "config") + widgets = [] + for widget_instance in hub.widgets: + if not widget_instance.enabled: + continue + if not widget_instance.allows(user, "view"): + continue + widgets.append(widget_instance.get_props( + with_secret_config=user_can_config)) + return flask.jsonify({"status": "OK", "data": widgets}) + + +@app.route('/api/hubs//widgets//', + methods=['GET', 'PUT', 'DELETE']) +@require_hub_access("view", url_param="hub", json=True) +def api_hub_widget(hub, idx): + widget_instance = get_widget_instance(hub, idx) + hub = widget_instance.hub + try: + user = flask.g.user + except AttributeError: + user = None + if flask.request.method == "GET": + user_can_config = hub.allows(user, "config") + result = widget_instance.get_props(with_secret_config=user_can_config) + return flask.jsonify({"status": "OK", "data": result}) + elif flask.request.method == "PUT": + check_hub_access(hub, "config", json=True) + request_data = flask.request.get_json() + widget_config = request_data.get("config", {}) + try: + configure_widget_instance(widget_instance, widget_config) + except WidgetConfigError as e: + return flask.jsonify({"status": "ERROR", "message": e.args[0]}) + if "index" in request_data: + # Reorder the widgets + move_widget(widget_instance, request_data["index"]) + try: + flask.g.db.commit() + except Exception as e: + msg = "Could not configure this widget: {}".format(e) + return flask.jsonify({"status": "ERROR", "message": msg}) + return flask.jsonify({"status": "OK"}) + elif flask.request.method == "DELETE": + # Remove the widget from the hub. + check_hub_access(hub, "config", json=True) + is_left = widget_instance.left + flask.g.db.delete(widget_instance) + try: + flask.g.db.commit() + except Exception as e: + msg = "Could not remove this widget: {}".format(e) + return flask.jsonify({"status": "ERROR", "message": msg}) + reorder_widgets(hub, is_left) + return flask.jsonify({"status": "OK"}) + flask.abort(400) diff --git a/hubs/views/hub.py b/hubs/views/hub.py index ff52725..6677a7e 100644 --- a/hubs/views/hub.py +++ b/hubs/views/hub.py @@ -1,13 +1,11 @@ from __future__ import unicode_literals, absolute_import -import datetime import flask import hubs.models from hubs.app import app from hubs.utils.views import ( - get_hub, get_sse_url, get_visible_widgets, login_required, - RequestValidator, require_hub_access, + get_hub, get_sse_url, get_menu_entries, get_visible_widgets, require_hub_access, ) @@ -24,269 +22,3 @@ def hub(name): edit=False, sse_url=get_sse_url("hub/{}".format(hub.name)), ) - - -@app.route('//json/') -@app.route('//json') -@require_hub_access("view", json=True) -def hub_json(name): - hub = get_hub(name) - response = flask.jsonify(hub.__json__()) - # TODO -- modify headers with response.headers['X-fedora-hubs-wat'] = 'foo' - return response - - -@app.route('//edit', methods=['GET', 'POST']) -@login_required -@require_hub_access("config") -def hub_edit(name): - hub = get_hub(name, load_config=True) - if flask.request.method == 'POST': - return hub_edit_post(hub) - else: - return hub_edit_get(hub) - - -def hub_edit_get(hub): - widgets = get_visible_widgets(hub) - return flask.render_template( - 'hubs.html', - hub=hub, - widgets=widgets, - edit=True, - sse_url=get_sse_url("hub/{}".format(hub.name)), - ) - - -def hub_edit_post(hub): - is_js = flask.request.form.get('js', False) - - error = False - - # Check the input submitted - # Right side - r_widget_ids = [ - w.strip().replace('widget-', '') - for w in flask.request.form.getlist('right_widgets[]') - if w.strip() - ] - try: - r_widget_ids = [int(w) for w in r_widget_ids] - except: - flask.flash('Invalid widget identifiers submitted', 'error') - error = True - - r_indexes = [ - i.strip() for i in flask.request.form.getlist('right_indexes[]') - if i.strip() - ] - - try: - r_indexes = [int(i) for i in r_indexes] - except: - if not is_js: - flask.flash('Invalid indexes submitted', 'error') - error = True - - if len(r_widget_ids) != len(r_indexes): - if not is_js: - flask.flash( - 'The number of indexes and the number of widgets are not of ' - 'the same length', 'error') - error = True - - # Left side - l_widget_ids = [ - w.strip().replace('widget-', '') - for w in flask.request.form.getlist('left_widgets[]') - if w.strip() - ] - try: - l_widget_ids = [int(w) for w in l_widget_ids] - except: - flask.flash('Invalid widget identifiers submitted', 'error') - error = True - - l_indexes = [ - i.strip() for i in flask.request.form.getlist('left_indexes[]') - if i.strip() - ] - - try: - l_indexes = [int(i) for i in l_indexes] - except: - if not is_js: - flask.flash('Invalid indexes submitted', 'error') - error = True - - if len(l_widget_ids) != len(l_indexes): - if not is_js: - flask.flash( - 'The number of indexes and the number of widgets are not of ' - 'the same length', 'error') - error = True - - # If all good, update the database - if not error: - for cnt, wid in enumerate(r_widget_ids): - widget = flask.g.db.query(hubs.models.Widget).get(wid) - if widget.index != r_indexes[cnt]: - widget.index = r_indexes[cnt] - flask.g.db.add(widget) - for cnt, wid in enumerate(l_widget_ids): - widget = flask.g.db.query(hubs.models.Widget).get(wid) - if widget.index != l_indexes[cnt]: - widget.index = l_indexes[cnt] - flask.g.db.add(widget) - hub.last_edited = datetime.datetime.utcnow() - flask.g.db.add(hub) - try: - flask.g.db.commit() - except Exception as err: - error = True - if not is_js: - flask.flash( - 'Could not save your changes to the database ' - 'if the error persists, please warn an admin', - 'error') - if is_js: - if error: - flask.abort(400) - else: - return ('ok', 200) - else: - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('//config', methods=['GET', 'POST']) -@login_required -@require_hub_access("config", json=True) -def hub_config(name): - hub = get_hub(name) - if flask.request.method == 'POST': - category = flask.request.args["category"] - if category == "config": - result = hub_config_post_config(hub) - if category == "role_change": - result = hub_config_post_role_change(hub) - if result is not None: - return result - result = hub_config_get(hub) - return flask.jsonify(result) - - -def hub_config_get(hub): - roles = ["owner", "member"] - result = { - "status": "OK", - "result": { - "hubconfig": hub.config.__json__(), - "general": { - "chat_networks": app.config["CHAT_NETWORKS"], - "hub_visibility": hubs.models.HubConfig.VISIBILITY, - "roles": roles, - }, - "users": {role: [] for role in roles}, - }, - } - for assoc in sorted(hub.associations, key=lambda a: a.user.username): - if assoc.role not in roles: - continue - user = { - "username": assoc.user.username, - "fullname": assoc.user.fullname, - "role": assoc.role, - } - if (assoc.role == "owner" and - assoc.user.username == flask.g.user.username): - user["locked"] = True - result["result"]["users"][assoc.role].append(user) - return result - - -def hub_config_post_config(hub): - # Validate values - def _validate_chat_domain(value): - valid_chat_domains = [ - network["domain"] for network in app.config["CHAT_NETWORKS"] - ] - if not value and len(valid_chat_domains) > 0: - value = valid_chat_domains[0] - if value not in valid_chat_domains: - raise ValueError("Unsupported chat domain.") - return value - - validator = RequestValidator(dict( - # Only allow the parameters listed in __json__(). - (key, None) for key in hub.config.__json__().keys() - )) - validator.converters["chat_domain"] = _validate_chat_domain - try: - values = validator(flask.request.form) - except ValueError as e: - result = {"status": "ERROR", "message": "Invalid value(s)"} - result["fields"] = e.args[0] - return flask.jsonify(result) - - # Now set the configuration values - for key, value in values.items(): - setattr(hub.config, key, value) - try: - flask.g.db.commit() - except Exception as err: - result = {"status": "ERROR", "message": str(err)} - return flask.jsonify(result) - - -def hub_config_post_role_change(hub): - username = flask.request.form["username"] - user = hubs.models.User.get(username=username) - if user is None: - result = { - "status": "ERROR", - "fields": {"username": "Unknown user"}, - "message": "Unknown user", - } - return flask.jsonify(result) - role = flask.request.form.get("role") - for assoc in hub.associations: - if assoc.hub == hub and assoc.user == user: - hub.associations.remove(assoc) - if role: - hub.associations.append(hubs.models.Association( - hub=hub, user=user, role=role)) - flask.g.db.commit() - - -@app.route('//config/suggest-users') -@login_required -@require_hub_access("config", json=True) -def hub_config_suggest_users(name): - MAX_SUGGESTS = 10 - hub = get_hub(name) - results = flask.g.db.query(hubs.models.User.username) - query = flask.request.args.get("q") - if query: - results = results.filter( - hubs.models.User.username.ilike("%{}%".format(query)) - ) - exclude = flask.request.args.get("exclude-role") - if exclude: - exclude_query = flask.g.db.query( - hubs.models.User.username - ).join( - hubs.models.Association - ).join( - hubs.models.Hub - ).filter( - hubs.models.Hub.name == hub.name, - hubs.models.Association.role == exclude, - ) - results = results.filter( - ~hubs.models.User.username.in_(exclude_query) - ) - results = results.order_by(hubs.models.User.username).limit(MAX_SUGGESTS) - return flask.jsonify({ - "status": "OK", - "results": [r[0] for r in results], - }) diff --git a/hubs/views/widget.py b/hubs/views/widget.py deleted file mode 100644 index 2cd7f98..0000000 --- a/hubs/views/widget.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import unicode_literals, absolute_import - -import flask - -from hubs.app import app -from hubs.widgets import registry -from pkg_resources import resource_isdir -from hubs.utils.views import ( - create_widget_instance, configure_widget_instance, get_hub, - get_widget_instance, login_required, WidgetConfigError, - get_position, require_hub_access, - ) - - -@app.route('//add', methods=['GET', 'POST']) -@login_required -@require_hub_access("config") -def hub_add_widget(name): - hub = get_hub(name) - position = get_position() - widgets = [ - widget - for widget in registry.values() - if widget.position in ['both', position] - ] - - if flask.request.method == 'POST': - widget_name = flask.request.form.get('widget') - if not widget_name: - flask.abort(400, 'Invalid request sent') - if widget_name not in [w.name for w in widgets]: - flask.abort(404, 'Unknown widget called') - widget = registry[widget_name] - if widget.get_parameters(): - return flask.jsonify({ - "status": "CONFIGURE", - "url": widget.get_add_url(hub, position), - }) - create_widget_instance(hub, widget, position) - return flask.jsonify({"status": "ADDED"}) - return flask.render_template( - 'add_widget.html', - hub=hub, - widgets=widgets, - position=position, - ) - - -@app.route('//add/', methods=['GET', 'POST']) -@login_required -@require_hub_access("config", url_param="hub") -def widget_add(hub, widget): - hub = get_hub(hub) - position = get_position() - try: - widget = registry[widget] - except KeyError: - flask.abort(404, 'Unknown widget: %s' % widget) - if flask.request.method == 'POST': - create_widget_instance(hub, widget, position) - return flask.jsonify({"status": "ADDED"}) - return flask.render_template( - 'widget_add.html', - hub=hub, - widget=widget, - position=position, - ) - - -@app.route('///edit', methods=['GET', 'POST']) -@login_required -@require_hub_access("config", url_param="hub") -def widget_edit(hub, idx): - widget_instance = get_widget_instance(hub, idx) - if flask.request.method == 'POST': - try: - configure_widget_instance(widget_instance) - except WidgetConfigError as e: - flask.flash(e.args[0], "error") - else: - flask.flash("The widget has been updated.") - return flask.redirect(flask.url_for('hub_edit', name=hub)) - else: - return flask.render_template( - 'widget_edit.html', - hub=widget_instance.hub, - widget_instance=widget_instance, - ) - - -@app.route('///delete', methods=['POST']) -@login_required -@require_hub_access("config", url_param="hub") -def widget_edit_delete(hub, idx): - ''' Remove a widget from a hub. ''' - widget_instance = get_widget_instance(hub, idx) - flask.g.db.delete(widget_instance) - try: - flask.g.db.commit() - except Exception: - flask.flash( - 'Could not delete this widget from this hub in the database ' - 'if the error persists, please warn an admin', - 'error') - return flask.redirect(flask.url_for('hub_edit', name=hub)) - - -@app.route('///json') -@require_hub_access("view", url_param="hub") -def widget_json(hub, idx): - widget = get_widget_instance(hub, idx) - response = flask.jsonify(widget.__json__()) - # TODO -- modify headers with response.headers['X-fedora-hubs-wat'] = 'foo' - return response - - -@app.route('/source/') -def widget_source(name): - try: - widget_path = registry[name].__module__ - except KeyError: - flask.abort(404) - widget_url = widget_path.replace(".", "/") - parent, _ignore, module = widget_path.rpartition(".") - if resource_isdir(parent, module): - widget_url += "/__init__.py" - else: - widget_url += ".py" - url = "/".join([app.config.get('SOURCE_URL'), widget_url]) - return flask.redirect(url) diff --git a/hubs/widgets/fedmsgstats/__init__.py b/hubs/widgets/fedmsgstats/__init__.py index 2cecb75..62fe099 100644 --- a/hubs/widgets/fedmsgstats/__init__.py +++ b/hubs/widgets/fedmsgstats/__init__.py @@ -37,9 +37,9 @@ class BaseView(RootWidgetView): context = dict( username=username, hub_subscribe_url=flask.url_for( - 'hub_subscribe', hub=instance.hub.name), + 'api_hub_subscribe', hub=instance.hub.name), hub_unsubscribe_url=flask.url_for( - 'hub_unsubscribe', hub=instance.hub.name), + 'api_hub_unsubscribe', hub=instance.hub.name), ) get_stats = GetStats(instance) context.update(get_stats()) diff --git a/hubs/widgets/stats/__init__.py b/hubs/widgets/stats/__init__.py index b893feb..589d0ce 100644 --- a/hubs/widgets/stats/__init__.py +++ b/hubs/widgets/stats/__init__.py @@ -41,13 +41,14 @@ class GetStats(CachedFunction): subscribers_text=commas(len(subscribers)), stargazers_text=commas(len(stargazers)), - hub_leave_url=flask.url_for('hub_leave', hub=hub.name), - hub_join_url=flask.url_for('hub_join', hub=hub.name), - hub_unstar_url=flask.url_for('hub_unstar', hub=hub.name), - hub_star_url=flask.url_for('hub_star', hub=hub.name), - hub_subscribe_url=flask.url_for('hub_subscribe', hub=hub.name), + hub_leave_url=flask.url_for('api_hub_leave', hub=hub.name), + hub_join_url=flask.url_for('api_hub_join', hub=hub.name), + hub_unstar_url=flask.url_for('api_hub_unstar', hub=hub.name), + hub_star_url=flask.url_for('api_hub_star', hub=hub.name), + hub_subscribe_url=flask.url_for( + 'api_hub_subscribe', hub=hub.name), hub_unsubscribe_url=flask.url_for( - 'hub_unsubscribe', hub=hub.name), + 'api_hub_unsubscribe', hub=hub.name), ) def should_invalidate(self, message): From 1389bb26b55cf2bb05b252f494489dd252d7b5df Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Sep 28 2017 17:35:09 +0000 Subject: [PATCH 9/20] Widget add and config panels are now React-based --- diff --git a/hubs/models.py b/hubs/models.py index 498d4fa..55530f9 100644 --- a/hubs/models.py +++ b/hubs/models.py @@ -458,9 +458,6 @@ class Widget(ObjectAuthzMixin, BASE): def module(self): return hubs.widgets.registry[self.plugin] - @property - def edit_url(self): - return self.module.get_edit_url(self.hub, self) @property def enabled(self): diff --git a/hubs/templates/add_widget.html b/hubs/templates/add_widget.html deleted file mode 100644 index 878c2fd..0000000 --- a/hubs/templates/add_widget.html +++ /dev/null @@ -1,30 +0,0 @@ - - diff --git a/hubs/templates/widget_add.html b/hubs/templates/widget_add.html deleted file mode 100644 index d3cf376..0000000 --- a/hubs/templates/widget_add.html +++ /dev/null @@ -1,49 +0,0 @@ - diff --git a/hubs/templates/widget_edit.html b/hubs/templates/widget_edit.html deleted file mode 100644 index fc4f8ce..0000000 --- a/hubs/templates/widget_edit.html +++ /dev/null @@ -1,59 +0,0 @@ - diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index b562662..698f34d 100644 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -202,45 +202,6 @@ class Widget(object): self.name, url_rule.lstrip("/")) app.add_url_rule(rule, view_func=view_func) - def get_add_url(self, hub, position): - """Returns the URL to the configuration panel to add this widget. - - By default, it returns the URL to the common widget configuration panel - that will simply display the widget's parameters in a form. Widgets - override this method to implement a more complex configuration view. - - If a more complex configuration view is implemented, the resulting HTML - code will be wrapped in a ``