From 3e7152bb9394d1f49c2d8a5716fe903a357df439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 11:30:24 +0100 Subject: [PATCH 01/31] Made requirements.txt py36-compatible Some packages had to be removed; they are deployment-specific anyway, and may not even be needed any more. I've also added some secondary requirements that weren't specified yet. The next steps will be to split into runtime and development requirements. --- requirements.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index fa1593b5..5343ed40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,12 +41,13 @@ pytest-cov==2.3.1 mock==2.0.0 # Secondary requirements -flup==1.0.2 Flask-PyMongo==0.4.1 Jinja2==2.8 Werkzeug==0.11.10 +appdirs==1.4.2 argparse==1.2.1 cffi==1.6.0 +CommonMark==0.7.2 cookies==2.2.1 cryptography==1.3.1 enum34==1.1.3 @@ -57,6 +58,8 @@ googleapis-common-protos==1.1.0 ipaddress==1.0.16 itsdangerous==0.24 oauth2client==2.0.2 +oauthlib==2.0.1 +packaging==16.8 pbr==1.9.1 protobuf==3.0.0b2.post2 protorpc==0.11.1 @@ -64,7 +67,8 @@ py==1.4.31 pyasn1==0.1.9 pyasn1-modules==0.0.8 pymongo==3.2.2 +pyparsing==2.1.10 requests_oauthlib==0.7.0 six==1.10.0 -wsgiref==0.1.2 coverage==4.0.3 +WTForms==2.1 From eadb91abc9405cc83029d45e230ffa2f1b4f37ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 11:37:55 +0100 Subject: [PATCH 02/31] Removed development requirements and unused secondary requirements. --- requirements.txt | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5343ed40..fe79316f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,41 +34,27 @@ WebOb==1.5.0 wheel==0.29.0 zencoder==0.6.5 -# Development requirements -pytest==3.0.1 -responses==0.5.1 -pytest-cov==2.3.1 -mock==2.0.0 - # Secondary requirements Flask-PyMongo==0.4.1 Jinja2==2.8 Werkzeug==0.11.10 appdirs==1.4.2 -argparse==1.2.1 cffi==1.6.0 CommonMark==0.7.2 -cookies==2.2.1 cryptography==1.3.1 -enum34==1.1.3 -funcsigs==1.0.1 future==0.15.2 html5lib==0.9999999 googleapis-common-protos==1.1.0 -ipaddress==1.0.16 itsdangerous==0.24 oauth2client==2.0.2 oauthlib==2.0.1 packaging==16.8 -pbr==1.9.1 protobuf==3.0.0b2.post2 protorpc==0.11.1 -py==1.4.31 pyasn1==0.1.9 pyasn1-modules==0.0.8 pymongo==3.2.2 pyparsing==2.1.10 -requests_oauthlib==0.7.0 +requests-oauthlib==0.7.0 six==1.10.0 -coverage==4.0.3 WTForms==2.1 From 2d6bdd350f1821ffcb26d2213093196de4747f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 11:42:23 +0100 Subject: [PATCH 03/31] Added development requirements to requirements-dev.txt In this commit (and the previous ones on requirements files) I haven't changed the package versions. Upgrading our dependencies is for another time. --- requirements-dev.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..1c5f40f6 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,12 @@ +-r requirements.txt + +# Development requirements +pytest==3.0.1 +responses==0.5.1 +pytest-cov==2.3.1 + +# Secondary development requirements +cookies==2.2.1 +coverage==4.0.3 +pbr==1.9.1 +py==1.4.31 From 7c055b5f565f9e44ad404e0bff17b9dc8912786e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:00:24 +0100 Subject: [PATCH 04/31] Ran 2to3 on pillar + some manual fixups The 'manual fixups' are: - incorrect use of dict.items() where dict.iteritems() was meant; this results in list(dict.items()), which I changed to dict.items(). - removal of 'from __future__ import' lines, which 2to3 changes into empty lines; I removed the empty lines. --- pillar/__init__.py | 8 +- pillar/api/blender_cloud/home_project.py | 14 +-- pillar/api/blender_id.py | 6 +- pillar/api/custom_field_validation.py | 8 +- pillar/api/eve_settings.py | 2 +- pillar/api/file_storage/__init__.py | 2 +- pillar/api/file_storage/moving.py | 2 +- pillar/api/local_auth.py | 2 +- pillar/api/nodes/__init__.py | 10 +- pillar/api/nodes/custom/comment.py | 24 ++-- pillar/api/projects/hooks.py | 4 +- pillar/api/projects/routes.py | 4 +- pillar/api/service.py | 10 +- pillar/api/users/hooks.py | 6 +- pillar/api/utils/__init__.py | 13 +- pillar/api/utils/authentication.py | 2 +- pillar/api/utils/authorization.py | 12 +- pillar/api/utils/gcs.py | 12 +- pillar/api/utils/imaging.py | 14 +-- pillar/auth/__init__.py | 4 +- pillar/cli.py | 26 ++-- pillar/config.py | 4 +- pillar/extension.py | 4 +- pillar/markdown.py | 2 - pillar/sdk.py | 6 +- pillar/tests/__init__.py | 12 +- pillar/tests/common_test_data.py | 146 +++++++++++------------ pillar/tests/config_testing.py | 2 +- pillar/web/jinja.py | 2 - pillar/web/main/routes.py | 4 +- pillar/web/nodes/attachments.py | 16 +-- pillar/web/nodes/custom/comments.py | 2 +- pillar/web/nodes/forms.py | 2 +- pillar/web/nodes/routes.py | 18 +-- pillar/web/projects/routes.py | 6 +- pillar/web/redirects/__init__.py | 4 +- pillar/web/users/routes.py | 12 +- pillar/web/utils/__init__.py | 10 +- pillar/web/utils/forms.py | 82 ++++++------- 39 files changed, 254 insertions(+), 265 deletions(-) diff --git a/pillar/__init__.py b/pillar/__init__.py index 2f149659..2e136a34 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -360,12 +360,12 @@ class PillarServer(Eve): if node_type: node_type = node_type.replace('_', ' ').title() if doc_name: - description = u'%s "%s" was deleted.' % (node_type, doc_name) + description = '%s "%s" was deleted.' % (node_type, doc_name) else: - description = u'This %s was deleted.' % (node_type, ) + description = 'This %s was deleted.' % (node_type, ) else: if doc_name: - description = u'"%s" was deleted.' % doc_name + description = '"%s" was deleted.' % doc_name else: description = None @@ -441,7 +441,7 @@ class PillarServer(Eve): web.setup_app(self) authentication.setup_app(self) - for ext in self.pillar_extensions.itervalues(): + for ext in self.pillar_extensions.values(): self.log.info('Setting up extension %s', ext.name) ext.setup_app(self) diff --git a/pillar/api/blender_cloud/home_project.py b/pillar/api/blender_cloud/home_project.py index 7c1566aa..4f764144 100644 --- a/pillar/api/blender_cloud/home_project.py +++ b/pillar/api/blender_cloud/home_project.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) HOME_PROJECT_USERS = set() # Users with any of these roles will get full write access to their home project. -HOME_PROJECT_WRITABLE_USERS = {u'subscriber', u'demo'} +HOME_PROJECT_WRITABLE_USERS = {'subscriber', 'demo'} HOME_PROJECT_DESCRIPTION = ('# Your home project\n\n' 'This is your home project. It allows synchronisation ' @@ -30,7 +30,7 @@ HOME_PROJECT_SUMMARY = 'This is your home project. Here you can sync your Blende # 'as a pastebin for text, images and other assets, and ' # 'allows synchronisation of your Blender settings.') # HOME_PROJECT_SUMMARY = 'This is your home project. Pastebin and Blender settings sync in one!' -SYNC_GROUP_NODE_NAME = u'Blender Sync' +SYNC_GROUP_NODE_NAME = 'Blender Sync' SYNC_GROUP_NODE_DESC = ('The [Blender Cloud Addon](https://cloud.blender.org/services' '#blender-addon) will synchronize your Blender settings here.') @@ -135,8 +135,8 @@ def create_home_project(user_id, write_access): # This allows people to comment on shared images and see comments. node_type_comment = assign_permissions( node_type_comment, - subscriber_methods=[u'GET', u'POST'], - world_methods=[u'GET']) + subscriber_methods=['GET', 'POST'], + world_methods=['GET']) project['node_types'] = [ node_type_group, @@ -215,7 +215,7 @@ def home_project(): write_access = write_access_with_roles(roles) create_home_project(user_id, write_access) - resp, _, _, status, _ = get('projects', category=u'home', user=user_id) + resp, _, _, status, _ = get('projects', category='home', user=user_id) if status != 200: return utils.jsonify(resp), status @@ -248,8 +248,8 @@ def home_project_permissions(write_access): """ if write_access: - return [u'GET', u'PUT', u'POST', u'DELETE'] - return [u'GET'] + return ['GET', 'PUT', 'POST', 'DELETE'] + return ['GET'] def has_home_project(user_id): diff --git a/pillar/api/blender_id.py b/pillar/api/blender_id.py index 57568017..86fc57d0 100644 --- a/pillar/api/blender_id.py +++ b/pillar/api/blender_id.py @@ -86,7 +86,7 @@ def upsert_user(db_user, blender_id_user_id): :type: (ObjectId, int) """ - if u'subscriber' in db_user.get('groups', []): + if 'subscriber' in db_user.get('groups', []): log.error('Non-ObjectID string found in user.groups: %s', db_user) raise wz_exceptions.InternalServerError('Non-ObjectID string found in user.groups: %s' % db_user) @@ -117,8 +117,8 @@ def upsert_user(db_user, blender_id_user_id): if status == 422: # Probably non-unique username, so retry a few times with different usernames. log.info('Error creating new user: %s', r) - username_issue = r.get('_issues', {}).get(u'username', '') - if u'not unique' in username_issue: + username_issue = r.get('_issues', {}).get('username', '') + if 'not unique' in username_issue: # Retry db_user['username'] = authentication.make_unique_username(db_user['email']) continue diff --git a/pillar/api/custom_field_validation.py b/pillar/api/custom_field_validation.py index 98929159..773acb82 100644 --- a/pillar/api/custom_field_validation.py +++ b/pillar/api/custom_field_validation.py @@ -61,13 +61,13 @@ class ValidateCustomFields(Validator): Only validates the dict values, not the keys. Modifies the given dict in-place. """ - assert dict_valueschema[u'type'] == u'dict' + assert dict_valueschema['type'] == 'dict' assert isinstance(dict_property, dict) for key, val in dict_property.items(): - item_schema = {u'item': dict_valueschema} - item_prop = {u'item': val} - dict_property[key] = self.convert_properties(item_prop, item_schema)[u'item'] + item_schema = {'item': dict_valueschema} + item_prop = {'item': val} + dict_property[key] = self.convert_properties(item_prop, item_schema)['item'] def _validate_valid_properties(self, valid_properties, field, value): from pillar.api.utils import project_get_node_type diff --git a/pillar/api/eve_settings.py b/pillar/api/eve_settings.py index 544ea367..a362671b 100644 --- a/pillar/api/eve_settings.py +++ b/pillar/api/eve_settings.py @@ -723,7 +723,7 @@ users = { # By default don't include the 'auth' field. It can still be obtained # using projections, though, so we block that in hooks. - 'datasource': {'projection': {u'auth': 0}}, + 'datasource': {'projection': {'auth': 0}}, 'schema': users_schema } diff --git a/pillar/api/file_storage/__init__.py b/pillar/api/file_storage/__init__.py index 6705ebd3..81fbb750 100644 --- a/pillar/api/file_storage/__init__.py +++ b/pillar/api/file_storage/__init__.py @@ -219,7 +219,7 @@ def process_file(gcs, file_id, local_file): mime_category, src_file['format'] = src_file['content_type'].split('/', 1) # Prevent video handling for non-admins. - if not user_has_role(u'admin') and mime_category == 'video': + if not user_has_role('admin') and mime_category == 'video': if src_file['format'].startswith('x-'): xified = src_file['format'] else: diff --git a/pillar/api/file_storage/moving.py b/pillar/api/file_storage/moving.py index cb3e1193..592be437 100644 --- a/pillar/api/file_storage/moving.py +++ b/pillar/api/file_storage/moving.py @@ -29,7 +29,7 @@ def change_file_storage_backend(file_id, dest_backend): Files on the original backend are not deleted automatically. """ - dest_backend = unicode(dest_backend) + dest_backend = str(dest_backend) file_id = ObjectId(file_id) # Fetch file document diff --git a/pillar/api/local_auth.py b/pillar/api/local_auth.py index 41d2bb66..0febed98 100644 --- a/pillar/api/local_auth.py +++ b/pillar/api/local_auth.py @@ -87,7 +87,7 @@ def generate_and_store_token(user_id, days=15, prefix=''): def hash_password(password, salt): - if isinstance(salt, unicode): + if isinstance(salt, str): salt = salt.encode('utf-8') encoded_password = base64.b64encode(hashlib.sha256(password).digest()) return bcrypt.hashpw(encoded_password, salt) diff --git a/pillar/api/nodes/__init__.py b/pillar/api/nodes/__init__.py index 5ac28ba6..fab323fa 100644 --- a/pillar/api/nodes/__init__.py +++ b/pillar/api/nodes/__init__.py @@ -1,7 +1,7 @@ import base64 import functools import logging -import urlparse +import urllib.parse import pymongo.errors import rsa.randnum @@ -20,7 +20,7 @@ from pillar.api.utils.gcs import update_file_name log = logging.getLogger(__name__) blueprint = Blueprint('nodes_api', __name__) -ROLES_FOR_SHARING = {u'subscriber', u'demo'} +ROLES_FOR_SHARING = {'subscriber', 'demo'} def only_for_node_type_decorator(*required_node_type_names): @@ -138,7 +138,7 @@ def make_world_gettable(node): log.debug('Ensuring the world can read node %s', node_id) world_perms = set(node.get('permissions', {}).get('world', [])) - world_perms.add(u'GET') + world_perms.add('GET') world_perms = list(world_perms) result = nodes_coll.update_one({'_id': node_id}, @@ -164,7 +164,7 @@ def create_short_code(node): def short_link_info(short_code): """Returns the short link info in a dict.""" - short_link = urlparse.urljoin(current_app.config['SHORT_LINK_BASE_URL'], short_code) + short_link = urllib.parse.urljoin(current_app.config['SHORT_LINK_BASE_URL'], short_code) return { 'short_code': short_code, @@ -349,7 +349,7 @@ def node_set_default_picture(node, original=None): # Find the colour map, defaulting to the first image map available. image_file_id = None for image in props.get('files', []): - if image_file_id is None or image.get('map_type') == u'color': + if image_file_id is None or image.get('map_type') == 'color': image_file_id = image.get('file') else: log.debug('Not setting default picture on node type %s content type %s', diff --git a/pillar/api/nodes/custom/comment.py b/pillar/api/nodes/custom/comment.py index 4842ce30..5d0a708a 100644 --- a/pillar/api/nodes/custom/comment.py +++ b/pillar/api/nodes/custom/comment.py @@ -11,20 +11,20 @@ from pillar.api.utils import authorization, authentication, jsonify from . import register_patch_handler log = logging.getLogger(__name__) -ROLES_FOR_COMMENT_VOTING = {u'subscriber', u'demo'} -COMMENT_VOTING_OPS = {u'upvote', u'downvote', u'revoke'} -VALID_COMMENT_OPERATIONS = COMMENT_VOTING_OPS.union({u'edit'}) +ROLES_FOR_COMMENT_VOTING = {'subscriber', 'demo'} +COMMENT_VOTING_OPS = {'upvote', 'downvote', 'revoke'} +VALID_COMMENT_OPERATIONS = COMMENT_VOTING_OPS.union({'edit'}) -@register_patch_handler(u'comment') +@register_patch_handler('comment') def patch_comment(node_id, patch): assert_is_valid_patch(node_id, patch) user_id = authentication.current_user_id() - if patch[u'op'] in COMMENT_VOTING_OPS: + if patch['op'] in COMMENT_VOTING_OPS: result, node = vote_comment(user_id, node_id, patch) else: - assert patch[u'op'] == u'edit', 'Invalid patch operation %s' % patch[u'op'] + assert patch['op'] == 'edit', 'Invalid patch operation %s' % patch['op'] result, node = edit_comment(user_id, node_id, patch) return jsonify({'_status': 'OK', @@ -95,9 +95,9 @@ def vote_comment(user_id, node_id, patch): return update actions = { - u'upvote': upvote, - u'downvote': downvote, - u'revoke': revoke, + 'upvote': upvote, + 'downvote': downvote, + 'revoke': revoke, } action = actions[patch['op']] mongo_update = action() @@ -141,7 +141,7 @@ def edit_comment(user_id, node_id, patch): log.warning('User %s wanted to patch non-existing node %s' % (user_id, node_id)) raise wz_exceptions.NotFound('Node %s not found' % node_id) - if node['user'] != user_id and not authorization.user_has_role(u'admin'): + if node['user'] != user_id and not authorization.user_has_role('admin'): raise wz_exceptions.Forbidden('You can only edit your own comments.') # Use Eve to PATCH this node, as that also updates the etag. @@ -173,8 +173,8 @@ def assert_is_valid_patch(node_id, patch): raise wz_exceptions.BadRequest("PATCH should have a key 'op' indicating the operation.") if op not in VALID_COMMENT_OPERATIONS: - raise wz_exceptions.BadRequest(u'Operation should be one of %s', - u', '.join(VALID_COMMENT_OPERATIONS)) + raise wz_exceptions.BadRequest('Operation should be one of %s', + ', '.join(VALID_COMMENT_OPERATIONS)) if op not in COMMENT_VOTING_OPS: # We can't check here, we need the node owner for that. diff --git a/pillar/api/projects/hooks.py b/pillar/api/projects/hooks.py index e6e1d3ce..e911b580 100644 --- a/pillar/api/projects/hooks.py +++ b/pillar/api/projects/hooks.py @@ -28,7 +28,7 @@ def before_inserting_projects(items): """ # Allow admin users to do whatever they want. - if user_has_role(u'admin'): + if user_has_role('admin'): return for item in items: @@ -70,7 +70,7 @@ def protect_sensitive_fields(document, original): """When not logged in as admin, prevents update to certain fields.""" # Allow admin users to do whatever they want. - if user_has_role(u'admin'): + if user_has_role('admin'): return def revert(name): diff --git a/pillar/api/projects/routes.py b/pillar/api/projects/routes.py index 57a53d04..f312059d 100644 --- a/pillar/api/projects/routes.py +++ b/pillar/api/projects/routes.py @@ -16,7 +16,7 @@ blueprint_api = Blueprint('projects_api', __name__) @blueprint_api.route('/create', methods=['POST']) -@authorization.require_login(require_roles={u'admin', u'subscriber', u'demo'}) +@authorization.require_login(require_roles={'admin', 'subscriber', 'demo'}) def create_project(overrides=None): """Creates a new project.""" @@ -65,7 +65,7 @@ def project_manage_users(): project = projects_collection.find_one({'_id': project_id}) # Check if the current_user is owner of the project, or removing themselves. - if not authorization.user_has_role(u'admin'): + if not authorization.user_has_role('admin'): remove_self = target_user_id == current_user_id and action == 'remove' if project['user'] != current_user_id and not remove_self: utils.abort_with_error(403) diff --git a/pillar/api/service.py b/pillar/api/service.py index da544e7a..ecd03a80 100644 --- a/pillar/api/service.py +++ b/pillar/api/service.py @@ -13,7 +13,7 @@ blueprint = Blueprint('service', __name__) log = logging.getLogger(__name__) signal_user_changed_role = blinker.NamedSignal('badger:user_changed_role') -ROLES_WITH_GROUPS = {u'admin', u'demo', u'subscriber'} +ROLES_WITH_GROUPS = {'admin', 'demo', 'subscriber'} # Map of role name to group ID, for the above groups. role_to_group_id = {} @@ -38,7 +38,7 @@ def fetch_role_to_group_id_map(): @blueprint.route('/badger', methods=['POST']) -@authorization.require_login(require_roles={u'service', u'badger'}, require_all=True) +@authorization.require_login(require_roles={'service', 'badger'}, require_all=True) def badger(): if request.mimetype != 'application/json': log.debug('Received %s instead of application/json', request.mimetype) @@ -117,7 +117,7 @@ def do_badger(action, user_email, role): @blueprint.route('/urler/', methods=['GET']) -@authorization.require_login(require_roles={u'service', u'urler'}, require_all=True) +@authorization.require_login(require_roles={'service', 'urler'}, require_all=True) def urler(project_id): """Returns the URL of any project.""" @@ -189,7 +189,7 @@ def create_service_account(email, roles, service, update_existing=None): raise ValueError('User %s already exists' % email) # Compute the new roles, and assign. - roles = list(set(roles).union({u'service'}).union(user['roles'])) + roles = list(set(roles).union({'service'}).union(user['roles'])) user['roles'] = list(roles) # Let the caller perform any required updates. @@ -204,7 +204,7 @@ def create_service_account(email, roles, service, update_existing=None): expected_status = 200 else: # Create a user with the correct roles. - roles = list(set(roles).union({u'service'})) + roles = list(set(roles).union({'service'})) user = {'username': email, 'groups': [], 'roles': roles, diff --git a/pillar/api/users/hooks.py b/pillar/api/users/hooks.py index 6a285ccf..24de19b6 100644 --- a/pillar/api/users/hooks.py +++ b/pillar/api/users/hooks.py @@ -60,7 +60,7 @@ def check_user_access(request, lookup): current_user_id = current_user['user_id'] if current_user else None # Admins can do anything and get everything, except the 'auth' block. - if user_has_role(u'admin'): + if user_has_role('admin'): return if not lookup and not current_user: @@ -74,7 +74,7 @@ def check_user_access(request, lookup): def check_put_access(request, lookup): """Only allow PUT to the current user, or all users if admin.""" - if user_has_role(u'admin'): + if user_has_role('admin'): return current_user = g.get('current_user') @@ -94,7 +94,7 @@ def after_fetching_user(user): current_user_id = current_user['user_id'] if current_user else None # Admins can do anything and get everything, except the 'auth' block. - if user_has_role(u'admin'): + if user_has_role('admin'): return # Only allow full access to the current user. diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index 59c36bdd..452e6ef5 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -1,7 +1,7 @@ import copy import hashlib import json -import urllib +import urllib.request, urllib.parse, urllib.error import datetime import functools @@ -103,7 +103,7 @@ def skip_when_testing(func): @functools.wraps(func) def wrapper(*args, **kwargs): if current_app.config['TESTING']: - log.debug('Skipping call to %s(...) due to TESTING', func.func_name) + log.debug('Skipping call to %s(...) due to TESTING', func.__name__) return None return func(*args, **kwargs) @@ -145,19 +145,18 @@ def gravatar(email, size=64): parameters = {'s': str(size), 'd': 'mm'} return "https://www.gravatar.com/avatar/" + \ hashlib.md5(str(email)).hexdigest() + \ - "?" + urllib.urlencode(parameters) + "?" + urllib.parse.urlencode(parameters) class MetaFalsey(type): - def __nonzero__(cls): + def __bool__(cls): return False __bool__ = __nonzero__ # for Python 3 -class DoesNotExist(object): +class DoesNotExist(object, metaclass=MetaFalsey): """Returned as value by doc_diff if a value does not exist.""" - __metaclass__ = MetaFalsey def doc_diff(doc1, doc2, falsey_is_equal=True): @@ -175,7 +174,7 @@ def doc_diff(doc1, doc2, falsey_is_equal=True): """ for key in set(doc1.keys()).union(set(doc2.keys())): - if isinstance(key, basestring) and key[0] == u'_': + if isinstance(key, str) and key[0] == '_': continue val1 = doc1.get(key, DoesNotExist) diff --git a/pillar/api/utils/authentication.py b/pillar/api/utils/authentication.py index 66003dc9..b58fd568 100644 --- a/pillar/api/utils/authentication.py +++ b/pillar/api/utils/authentication.py @@ -124,7 +124,7 @@ def store_token(user_id, token, token_expiry, oauth_subclient_id=False): :returns: the token document from MongoDB """ - assert isinstance(token, (str, unicode)), 'token must be string type, not %r' % type(token) + assert isinstance(token, str), 'token must be string type, not %r' % type(token) token_data = { 'user': user_id, diff --git a/pillar/api/utils/authorization.py b/pillar/api/utils/authorization.py index 9d3c77d4..0902e03a 100644 --- a/pillar/api/utils/authorization.py +++ b/pillar/api/utils/authorization.py @@ -238,22 +238,22 @@ def merge_permissions(*args): asdict0 = {permission[field_name]: permission['methods'] for permission in from0} asdict1 = {permission[field_name]: permission['methods'] for permission in from1} - keys = set(asdict0.keys() + asdict1.keys()) + keys = set(asdict0.keys()).union(set(asdict1.keys())) for key in maybe_sorted(keys): methods0 = asdict0.get(key, []) methods1 = asdict1.get(key, []) methods = maybe_sorted(set(methods0).union(set(methods1))) - effective.setdefault(plural_name, []).append({field_name: key, u'methods': methods}) + effective.setdefault(plural_name, []).append({field_name: key, 'methods': methods}) - merge(u'user') - merge(u'group') + merge('user') + merge('group') # Gather permissions for world world0 = args[0].get('world', []) world1 = args[1].get('world', []) world_methods = set(world0).union(set(world1)) if world_methods: - effective[u'world'] = maybe_sorted(world_methods) + effective['world'] = maybe_sorted(world_methods) # Recurse for longer merges if len(args) > 2: @@ -380,4 +380,4 @@ def user_matches_roles(require_roles=set(), def is_admin(user): """Returns True iff the given user has the admin role.""" - return user_has_role(u'admin', user) + return user_has_role('admin', user) diff --git a/pillar/api/utils/gcs.py b/pillar/api/utils/gcs.py index 60c2f5c6..12dd3b78 100644 --- a/pillar/api/utils/gcs.py +++ b/pillar/api/utils/gcs.py @@ -166,7 +166,7 @@ class GoogleCloudStorageBucket(object): """Set the ContentDisposition metadata so that when a file is downloaded it has a human-readable name. """ - blob.content_disposition = u'attachment; filename="{0}"'.format(name) + blob.content_disposition = 'attachment; filename="{0}"'.format(name) blob.patch() def copy_blob(self, blob, to_bucket): @@ -188,11 +188,11 @@ def update_file_name(node): if node['properties'].get('status', '') == 'processing': return - def _format_name(name, override_ext, size=None, map_type=u''): + def _format_name(name, override_ext, size=None, map_type=''): root, _ = os.path.splitext(name) - size = u'-{}'.format(size) if size else u'' - map_type = u'-{}'.format(map_type) if map_type else u'' - return u'{}{}{}{}'.format(root, size, map_type, override_ext) + size = '-{}'.format(size) if size else '' + map_type = '-{}'.format(map_type) if map_type else '' + return '{}{}{}{}'.format(root, size, map_type, override_ext) def _update_name(file_id, file_props): files_collection = current_app.data.driver.db['files'] @@ -202,7 +202,7 @@ def update_file_name(node): return # For textures -- the map type should be part of the name. - map_type = file_props.get('map_type', u'') + map_type = file_props.get('map_type', '') storage = GoogleCloudStorageBucket(str(node['project'])) blob = storage.Get(file_doc['file_path'], to_dict=False) diff --git a/pillar/api/utils/imaging.py b/pillar/api/utils/imaging.py index 10d93496..af92480a 100644 --- a/pillar/api/utils/imaging.py +++ b/pillar/api/utils/imaging.py @@ -21,7 +21,7 @@ def generate_local_thumbnails(name_base, src): save_to_base, _ = os.path.splitext(src) name_base, _ = os.path.splitext(name_base) - for size, settings in thumbnail_settings.iteritems(): + for size, settings in thumbnail_settings.items(): dst = '{0}-{1}{2}'.format(save_to_base, size, '.jpg') name = '{0}-{1}{2}'.format(name_base, size, '.jpg') @@ -143,7 +143,7 @@ def get_video_data(filepath): res_y=video_stream['height'], ) if video_stream['sample_aspect_ratio'] != '1:1': - print '[warning] Pixel aspect ratio is not square!' + print('[warning] Pixel aspect ratio is not square!') return outdata @@ -190,14 +190,14 @@ def ffmpeg_encode(src, format, res_y=720): dst = os.path.splitext(src) dst = "{0}-{1}p.{2}".format(dst[0], res_y, format) args.append(dst) - print "Encoding {0} to {1}".format(src, format) + print("Encoding {0} to {1}".format(src, format)) returncode = subprocess.call([current_app.config['BIN_FFMPEG']] + args) if returncode == 0: - print "Successfully encoded {0}".format(dst) + print("Successfully encoded {0}".format(dst)) else: - print "Error during encode" - print "Code: {0}".format(returncode) - print "Command: {0}".format(current_app.config['BIN_FFMPEG'] + " " + " ".join(args)) + print("Error during encode") + print("Code: {0}".format(returncode)) + print("Command: {0}".format(current_app.config['BIN_FFMPEG'] + " " + " ".join(args))) dst = None # return path of the encoded video return dst diff --git a/pillar/auth/__init__.py b/pillar/auth/__init__.py index 313ac30d..72167a83 100644 --- a/pillar/auth/__init__.py +++ b/pillar/auth/__init__.py @@ -55,11 +55,11 @@ def _load_user(token): login_user = UserClass(token) login_user.email = db_user['email'] - login_user.objectid = unicode(db_user['_id']) + login_user.objectid = str(db_user['_id']) login_user.username = db_user['username'] login_user.gravatar = utils.gravatar(db_user['email']) login_user.roles = db_user.get('roles', []) - login_user.groups = [unicode(g) for g in db_user['groups'] or ()] + login_user.groups = [str(g) for g in db_user['groups'] or ()] login_user.full_name = db_user.get('full_name', '') return login_user diff --git a/pillar/cli.py b/pillar/cli.py index 3842b858..57bf2a96 100644 --- a/pillar/cli.py +++ b/pillar/cli.py @@ -3,7 +3,7 @@ Run commands with 'flask ' """ -from __future__ import print_function, division + import copy import logging @@ -57,7 +57,7 @@ def setup_db(admin_email): print("Created user {0}".format(user['_id'])) # Create a default project by faking a POST request. - with current_app.test_request_context(data={'project_name': u'Default Project'}): + with current_app.test_request_context(data={'project_name': 'Default Project'}): from flask import g from pillar.api.projects import routes as proj_routes @@ -89,7 +89,7 @@ def find_duplicate_users(): blender_id = blender_ids[0] found_users[blender_id].append(user) - for blender_id, users in found_users.iteritems(): + for blender_id, users in found_users.items(): if len(users) == 1: continue @@ -344,14 +344,14 @@ def create_badger_account(email, badges): this account can assign and revoke. """ - create_service_account(email, [u'badger'], {'badger': badges.strip().split()}) + create_service_account(email, ['badger'], {'badger': badges.strip().split()}) @manager_setup.command def create_urler_account(email): """Creates a new service account that can fetch all project URLs.""" - create_service_account(email, [u'urler'], {}) + create_service_account(email, ['urler'], {}) @manager_setup.command @@ -704,7 +704,7 @@ def upgrade_attachment_schema(proj_url=None, all_projects=False): 'properties.attachments': {'$exists': True}, }) for node in nodes: - attachments = node[u'properties'][u'attachments'] + attachments = node['properties']['attachments'] if isinstance(attachments, dict): # This node has already been upgraded. continue @@ -713,9 +713,9 @@ def upgrade_attachment_schema(proj_url=None, all_projects=False): new_atts = {} for field_info in attachments: for attachment in field_info.get('files', []): - new_atts[attachment[u'slug']] = {u'oid': attachment[u'file']} + new_atts[attachment['slug']] = {'oid': attachment['file']} - node[u'properties'][u'attachments'] = new_atts + node['properties']['attachments'] = new_atts # Use Eve to PUT, so we have schema checking. db_node = remove_private_keys(node) @@ -774,11 +774,11 @@ def create_blog(proj_url): blog = nodes_coll.find_one({'node_type': 'blog', 'project': proj_id}) if not blog: blog = { - u'node_type': node_type_blog['name'], - u'name': u'Blog', - u'description': u'', - u'properties': {}, - u'project': proj_id, + 'node_type': node_type_blog['name'], + 'name': 'Blog', + 'description': '', + 'properties': {}, + 'project': proj_id, } r, _, _, status = post_internal('nodes', blog) if status != 201: diff --git a/pillar/config.py b/pillar/config.py index 4314a70b..835a0fac 100644 --- a/pillar/config.py +++ b/pillar/config.py @@ -79,7 +79,7 @@ FILE_LINK_VALIDITY = defaultdict( ) # Roles with full GET-access to all variations of files. -FULL_FILE_ACCESS_ROLES = {u'admin', u'subscriber', u'demo'} +FULL_FILE_ACCESS_ROLES = {'admin', 'subscriber', 'demo'} # Client and Subclient IDs for Blender ID BLENDER_ID_CLIENT_ID = 'SPECIAL-SNOWFLAKE-57' @@ -117,7 +117,7 @@ SHORT_CODE_LENGTH = 6 # characters # People are allowed this many bytes per uploaded file. FILESIZE_LIMIT_BYTES_NONSUBS = 32 * 2 ** 20 # Unless they have one of those roles. -ROLES_FOR_UNLIMITED_UPLOADS = {u'subscriber', u'demo', u'admin'} +ROLES_FOR_UNLIMITED_UPLOADS = {'subscriber', 'demo', 'admin'} ############################################# diff --git a/pillar/extension.py b/pillar/extension.py index 34f7bfed..80732c99 100644 --- a/pillar/extension.py +++ b/pillar/extension.py @@ -18,9 +18,7 @@ can then be registered to the application at app creation time: import abc -class PillarExtension(object): - __metaclass__ = abc.ABCMeta - +class PillarExtension(object, metaclass=abc.ABCMeta): @abc.abstractproperty def name(self): """The name of this extension. diff --git a/pillar/markdown.py b/pillar/markdown.py index 4b06cdad..a0a39233 100644 --- a/pillar/markdown.py +++ b/pillar/markdown.py @@ -3,8 +3,6 @@ This is for user-generated stuff, like comments. """ -from __future__ import absolute_import - import bleach import CommonMark diff --git a/pillar/sdk.py b/pillar/sdk.py index 47017e7b..bb8260cc 100644 --- a/pillar/sdk.py +++ b/pillar/sdk.py @@ -1,7 +1,7 @@ """PillarSDK subclass for direct Flask-internal calls.""" import logging -import urlparse +import urllib.parse from flask import current_app import pillarsdk @@ -23,8 +23,8 @@ class FlaskInternalApi(pillarsdk.Api): self.requests_to_flask_kwargs(kwargs) # Leave out the query string and fragment from the URL. - split_url = urlparse.urlsplit(url) - path = urlparse.urlunsplit(split_url[:-2] + (None, None)) + split_url = urllib.parse.urlsplit(url) + path = urllib.parse.urlunsplit(split_url[:-2] + (None, None)) try: response = client.open(path=path, query_string=split_url.query, method=method, **kwargs) diff --git a/pillar/tests/__init__.py b/pillar/tests/__init__.py index d84ce060..9323c258 100644 --- a/pillar/tests/__init__.py +++ b/pillar/tests/__init__.py @@ -1,9 +1,5 @@ # -*- encoding: utf-8 -*- -from __future__ import print_function - -from __future__ import absolute_import - import base64 import copy import json @@ -16,7 +12,7 @@ import sys try: from urllib.parse import urlencode except ImportError: - from urllib import urlencode + from urllib.parse import urlencode from bson import ObjectId, tz_util @@ -46,7 +42,7 @@ MY_PATH = os.path.dirname(os.path.abspath(__file__)) TEST_EMAIL_USER = 'koro' TEST_EMAIL_ADDRESS = '%s@testing.blender.org' % TEST_EMAIL_USER -TEST_FULL_NAME = u'врач Сергей' +TEST_FULL_NAME = 'врач Сергей' TEST_SUBCLIENT_TOKEN = 'my-subclient-token-for-pillar' BLENDER_ID_USER_RESPONSE = {'status': 'success', 'user': {'email': TEST_EMAIL_ADDRESS, @@ -207,9 +203,9 @@ class AbstractPillarTest(TestMinimal): 'roles': list(roles), 'settings': {'email_communications': 1}, 'auth': [{'token': '', - 'user_id': unicode(ctd.BLENDER_ID_TEST_USERID), + 'user_id': str(ctd.BLENDER_ID_TEST_USERID), 'provider': 'blender-id'}], - 'full_name': u'คนรักของผัดไทย', + 'full_name': 'คนรักของผัดไทย', 'email': TEST_EMAIL_ADDRESS }) diff --git a/pillar/tests/common_test_data.py b/pillar/tests/common_test_data.py index e39ad214..8f3a97cb 100644 --- a/pillar/tests/common_test_data.py +++ b/pillar/tests/common_test_data.py @@ -11,55 +11,55 @@ EXAMPLE_PROJECT_READONLY_GROUP2_ID = ObjectId('564733b56dcaf85da2faee8a') EXAMPLE_PROJECT_ID = ObjectId('5672beecc0261b2005ed1a33') EXAMPLE_PROJECT_OWNER_ID = ObjectId('552b066b41acdf5dec4436f2') -EXAMPLE_FILE = {u'_id': ObjectId('5672e2c1c379cf0007b31995'), - u'_updated': datetime.datetime(2016, 3, 25, 10, 28, 24, tzinfo=tz_util.utc), - u'height': 2048, - u'name': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4.png', u'format': 'png', - u'variations': [ - {u'format': 'jpg', u'height': 160, u'width': 160, u'length': 8558, - u'link': 'http://localhost:8002/file-variant-h', u'content_type': 'image/jpeg', - u'md5': '--', u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-b.jpg', - u'size': 'b'}, - {u'format': 'jpg', u'height': 2048, u'width': 2048, u'length': 819569, - u'link': 'http://localhost:8002/file-variant-h', u'content_type': 'image/jpeg', - u'md5': '--', u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-h.jpg', - u'size': 'h'}, - {u'format': 'jpg', u'height': 64, u'width': 64, u'length': 8195, - u'link': 'http://localhost:8002/file-variant-t', u'content_type': 'image/jpeg', - u'md5': '--', u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-t.jpg', - u'size': 't'}, +EXAMPLE_FILE = {'_id': ObjectId('5672e2c1c379cf0007b31995'), + '_updated': datetime.datetime(2016, 3, 25, 10, 28, 24, tzinfo=tz_util.utc), + 'height': 2048, + 'name': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4.png', 'format': 'png', + 'variations': [ + {'format': 'jpg', 'height': 160, 'width': 160, 'length': 8558, + 'link': 'http://localhost:8002/file-variant-h', 'content_type': 'image/jpeg', + 'md5': '--', 'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-b.jpg', + 'size': 'b'}, + {'format': 'jpg', 'height': 2048, 'width': 2048, 'length': 819569, + 'link': 'http://localhost:8002/file-variant-h', 'content_type': 'image/jpeg', + 'md5': '--', 'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-h.jpg', + 'size': 'h'}, + {'format': 'jpg', 'height': 64, 'width': 64, 'length': 8195, + 'link': 'http://localhost:8002/file-variant-t', 'content_type': 'image/jpeg', + 'md5': '--', 'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-t.jpg', + 'size': 't'}, ], - u'filename': 'brick_dutch_soft_bump.png', - u'project': EXAMPLE_PROJECT_ID, - u'width': 2048, u'length': 6227670, - u'user': ObjectId('56264fc4fa3a250344bd10c5'), - u'content_type': 'image/png', - u'_etag': '044ce3aede2e123e261c0d8bd77212f264d4f7b0', - u'_created': datetime.datetime(2015, 12, 17, 16, 28, 49, tzinfo=tz_util.utc), - u'md5': '', - u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4.png', - u'backend': 'pillar', - u'link': 'http://localhost:8002/file', - u'link_expires': datetime.datetime(2016, 3, 22, 9, 28, 22, tzinfo=tz_util.utc)} + 'filename': 'brick_dutch_soft_bump.png', + 'project': EXAMPLE_PROJECT_ID, + 'width': 2048, 'length': 6227670, + 'user': ObjectId('56264fc4fa3a250344bd10c5'), + 'content_type': 'image/png', + '_etag': '044ce3aede2e123e261c0d8bd77212f264d4f7b0', + '_created': datetime.datetime(2015, 12, 17, 16, 28, 49, tzinfo=tz_util.utc), + 'md5': '', + 'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4.png', + 'backend': 'pillar', + 'link': 'http://localhost:8002/file', + 'link_expires': datetime.datetime(2016, 3, 22, 9, 28, 22, tzinfo=tz_util.utc)} EXAMPLE_PROJECT = { - u'_created': datetime.datetime(2015, 12, 17, 13, 22, 56, tzinfo=tz_util.utc), - u'_etag': u'cc4643e98d3606f87bbfaaa200bfbae941b642f3', - u'_id': EXAMPLE_PROJECT_ID, - u'_updated': datetime.datetime(2016, 1, 7, 18, 59, 4, tzinfo=tz_util.utc), - u'category': u'assets', - u'description': u'Welcome to this curated collection of Blender Institute textures and image ' - u'resources. This collection is an on-going project, as with each project we ' - u'create a number of textures based on our own resources (photographs, scans, ' - u'etc.) or made completely from scratch. At the moment you can find all the ' - u'textures from the past Open Projects that were deemed re-usable. \r\n\r\n' - u'People who have contributed to these textures:\r\n\r\nAndrea Weikert, Andy ' - u'Goralczyk, Basse Salmela, Ben Dansie, Campbell Barton, Enrico Valenza, Ian ' - u'Hubert, Kjartan Tysdal, Manu J\xe4rvinen, Massimiliana Pulieso, Matt Ebb, ' - u'Pablo Vazquez, Rob Tuytel, Roland Hess, Sarah Feldlaufer, S\xf6nke M\xe4ter', - u'is_private': False, - u'name': u'Unittest project', - u'node_types': [ + '_created': datetime.datetime(2015, 12, 17, 13, 22, 56, tzinfo=tz_util.utc), + '_etag': 'cc4643e98d3606f87bbfaaa200bfbae941b642f3', + '_id': EXAMPLE_PROJECT_ID, + '_updated': datetime.datetime(2016, 1, 7, 18, 59, 4, tzinfo=tz_util.utc), + 'category': 'assets', + 'description': 'Welcome to this curated collection of Blender Institute textures and image ' + 'resources. This collection is an on-going project, as with each project we ' + 'create a number of textures based on our own resources (photographs, scans, ' + 'etc.) or made completely from scratch. At the moment you can find all the ' + 'textures from the past Open Projects that were deemed re-usable. \r\n\r\n' + 'People who have contributed to these textures:\r\n\r\nAndrea Weikert, Andy ' + 'Goralczyk, Basse Salmela, Ben Dansie, Campbell Barton, Enrico Valenza, Ian ' + 'Hubert, Kjartan Tysdal, Manu J\xe4rvinen, Massimiliana Pulieso, Matt Ebb, ' + 'Pablo Vazquez, Rob Tuytel, Roland Hess, Sarah Feldlaufer, S\xf6nke M\xe4ter', + 'is_private': False, + 'name': 'Unittest project', + 'node_types': [ PILLAR_NAMED_NODE_TYPES['group_texture'], PILLAR_NAMED_NODE_TYPES['group'], PILLAR_NAMED_NODE_TYPES['asset'], @@ -69,36 +69,36 @@ EXAMPLE_PROJECT = { PILLAR_NAMED_NODE_TYPES['post'], PILLAR_NAMED_NODE_TYPES['texture'], ], - u'nodes_blog': [], - u'nodes_featured': [], - u'nodes_latest': [], - u'permissions': {u'groups': [{u'group': EXAMPLE_ADMIN_GROUP_ID, - u'methods': [u'GET', u'POST', u'PUT', u'DELETE']}], - u'users': [], - u'world': [u'GET']}, - u'picture_header': ObjectId('5673f260c379cf0007b31bc4'), - u'picture_square': ObjectId('5673f256c379cf0007b31bc3'), - u'status': u'published', - u'summary': u'Texture collection from all Blender Institute open projects.', - u'url': u'textures', - u'user': EXAMPLE_PROJECT_OWNER_ID} + 'nodes_blog': [], + 'nodes_featured': [], + 'nodes_latest': [], + 'permissions': {'groups': [{'group': EXAMPLE_ADMIN_GROUP_ID, + 'methods': ['GET', 'POST', 'PUT', 'DELETE']}], + 'users': [], + 'world': ['GET']}, + 'picture_header': ObjectId('5673f260c379cf0007b31bc4'), + 'picture_square': ObjectId('5673f256c379cf0007b31bc3'), + 'status': 'published', + 'summary': 'Texture collection from all Blender Institute open projects.', + 'url': 'textures', + 'user': EXAMPLE_PROJECT_OWNER_ID} EXAMPLE_NODE = { - u'_id': ObjectId('572761099837730efe8e120d'), - u'picture': ObjectId('572761f39837730efe8e1210'), - u'description': u'', - u'node_type': u'asset', - u'user': ObjectId('57164ca1983773118cbaf779'), - u'properties': { - u'status': u'published', - u'content_type': u'image', - u'file': ObjectId('572761129837730efe8e120e') + '_id': ObjectId('572761099837730efe8e120d'), + 'picture': ObjectId('572761f39837730efe8e1210'), + 'description': '', + 'node_type': 'asset', + 'user': ObjectId('57164ca1983773118cbaf779'), + 'properties': { + 'status': 'published', + 'content_type': 'image', + 'file': ObjectId('572761129837730efe8e120e') }, - u'_updated': datetime.datetime(2016, 5, 2, 14, 19, 58, 0, tzinfo=tz_util.utc), - u'name': u'Image test', - u'project': EXAMPLE_PROJECT_ID, - u'_created': datetime.datetime(2016, 5, 2, 14, 19, 37, 0, tzinfo=tz_util.utc), - u'_etag': u'6b8589b42c880e3626f43f3e82a5c5b946742687' + '_updated': datetime.datetime(2016, 5, 2, 14, 19, 58, 0, tzinfo=tz_util.utc), + 'name': 'Image test', + 'project': EXAMPLE_PROJECT_ID, + '_created': datetime.datetime(2016, 5, 2, 14, 19, 37, 0, tzinfo=tz_util.utc), + '_etag': '6b8589b42c880e3626f43f3e82a5c5b946742687' } BLENDER_ID_TEST_USERID = 1533 diff --git a/pillar/tests/config_testing.py b/pillar/tests/config_testing.py index dbb020de..f56facac 100644 --- a/pillar/tests/config_testing.py +++ b/pillar/tests/config_testing.py @@ -8,4 +8,4 @@ TESTING = True CDN_STORAGE_USER = 'u41508580125621' FILESIZE_LIMIT_BYTES_NONSUBS = 20 * 2 ** 10 -ROLES_FOR_UNLIMITED_UPLOADS = {u'subscriber', u'demo', u'admin'} +ROLES_FOR_UNLIMITED_UPLOADS = {'subscriber', 'demo', 'admin'} diff --git a/pillar/web/jinja.py b/pillar/web/jinja.py index 737e3d29..9a374d97 100644 --- a/pillar/web/jinja.py +++ b/pillar/web/jinja.py @@ -1,7 +1,5 @@ """Our custom Jinja filters and other template stuff.""" -from __future__ import absolute_import - import logging import flask diff --git a/pillar/web/main/routes.py b/pillar/web/main/routes.py index 4947be8e..39e2b04f 100644 --- a/pillar/web/main/routes.py +++ b/pillar/web/main/routes.py @@ -304,8 +304,8 @@ def feeds_blogs(): updated = post._updated if post._updated else post._created url = url_for_node(node=post) content = post.properties.content[:500] - content = u'

{0}... Read more

'.format(content, url) - feed.add(post.name, unicode(content), + content = '

{0}... Read more

'.format(content, url) + feed.add(post.name, str(content), content_type='html', author=author, url=url, diff --git a/pillar/web/nodes/attachments.py b/pillar/web/nodes/attachments.py index b336b85f..69ea7f36 100644 --- a/pillar/web/nodes/attachments.py +++ b/pillar/web/nodes/attachments.py @@ -25,7 +25,7 @@ def render_attachments(node, field_value): node_attachments = node.properties.attachments or {} if isinstance(node_attachments, list): log.warning('Old-style attachments property found on node %s. Ignoring them, ' - 'will result in attachments not being found.', node[u'_id']) + 'will result in attachments not being found.', node['_id']) return field_value if not node_attachments: @@ -37,7 +37,7 @@ def render_attachments(node, field_value): try: att = node_attachments[slug] except KeyError: - return u'[attachment "%s" not found]' % slug + return '[attachment "%s" not found]' % slug return render_attachment(att) return shortcode_re.sub(replace, field_value) @@ -46,8 +46,8 @@ def render_attachments(node, field_value): def render_attachment(attachment): """Renders an attachment as HTML""" - oid = ObjectId(attachment[u'oid']) - collection = attachment.collection or u'files' + oid = ObjectId(attachment['oid']) + collection = attachment.collection or 'files' renderers = { 'files': render_attachment_file @@ -56,8 +56,8 @@ def render_attachment(attachment): try: renderer = renderers[collection] except KeyError: - log.error(u'Unable to render attachment from collection %s', collection) - return u'Unable to render attachment' + log.error('Unable to render attachment from collection %s', collection) + return 'Unable to render attachment' return renderer(attachment) @@ -66,7 +66,7 @@ def render_attachment_file(attachment): """Renders a file attachment.""" api = system_util.pillar_api() - sdk_file = pillarsdk.File.find(attachment[u'oid'], api=api) + sdk_file = pillarsdk.File.find(attachment['oid'], api=api) file_renderers = { 'image': render_attachment_file_image @@ -119,7 +119,7 @@ def attachment_form_group_set_data(db_prop_value, schema_prop, field_list): while len(field_list): field_list.pop_entry() - for slug, att_data in sorted(db_prop_value.iteritems()): + for slug, att_data in sorted(db_prop_value.items()): file_select_form_group = _attachment_build_single_field(schema_prop) subform = file_select_form_group() diff --git a/pillar/web/nodes/custom/comments.py b/pillar/web/nodes/custom/comments.py index fcb89e49..a44ac645 100644 --- a/pillar/web/nodes/custom/comments.py +++ b/pillar/web/nodes/custom/comments.py @@ -243,7 +243,7 @@ def comments_rate(comment_id, operation): """ - if operation not in {u'revoke', u'upvote', u'downvote'}: + if operation not in {'revoke', 'upvote', 'downvote'}: raise wz_exceptions.BadRequest('Invalid operation') api = system_util.pillar_api() diff --git a/pillar/web/nodes/forms.py b/pillar/web/nodes/forms.py index 6edaa9ef..00e97ec0 100644 --- a/pillar/web/nodes/forms.py +++ b/pillar/web/nodes/forms.py @@ -33,7 +33,7 @@ def iter_node_properties(node_type): node_schema = node_type['dyn_schema'].to_dict() form_schema = node_type['form_schema'].to_dict() - for prop_name, prop_schema in node_schema.iteritems(): + for prop_name, prop_schema in node_schema.items(): prop_fschema = form_schema.get(prop_name, {}) if not prop_fschema.get('visible', True): diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index 11ef6f6f..f39f1a93 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -142,7 +142,7 @@ def view(node_id): return 'GET' in node.permissions.world if current_user.is_authenticated: - allowed_roles = {u'subscriber', u'demo', u'admin'} + allowed_roles = {'subscriber', 'demo', 'admin'} return bool(allowed_roles.intersection(current_user.roles or ())) return False @@ -330,7 +330,7 @@ def edit(node_id): log.debug('set_properties(..., prefix=%r, set_data=%r) called', prefix, set_data) - for prop, schema_prop in dyn_schema.iteritems(): + for prop, schema_prop in dyn_schema.items(): prop_name = "{0}{1}".format(prefix, prop) if prop_name not in form: @@ -376,7 +376,7 @@ def edit(node_id): for file_data in db_prop_value: file_form_class = build_file_select_form(subschema) subform = file_form_class() - for key, value in file_data.iteritems(): + for key, value in file_data.items(): setattr(subform, key, value) field_list.append_entry(subform) @@ -481,7 +481,7 @@ def ensure_lists_exist_as_empty(node_doc, node_type): node_properties = node_doc.setdefault('properties', {}) - for prop, schema in node_type.dyn_schema.to_dict().iteritems(): + for prop, schema in node_type.dyn_schema.to_dict().items(): if schema['type'] != 'list': continue @@ -563,7 +563,7 @@ def redirect_to_context(node_id): def url_for_node(node_id=None, node=None): - assert isinstance(node_id, (basestring, type(None))) + assert isinstance(node_id, (str, type(None))) api = system_util.pillar_api() @@ -583,7 +583,7 @@ def url_for_node(node_id=None, node=None): # Import of custom modules (using the same nodes decorator) -import custom.comments -import custom.groups -import custom.storage -import custom.posts +from . import custom.comments +from . import custom.groups +from . import custom.storage +from . import custom.posts diff --git a/pillar/web/projects/routes.py b/pillar/web/projects/routes.py index 5f33bb45..676fd109 100644 --- a/pillar/web/projects/routes.py +++ b/pillar/web/projects/routes.py @@ -449,7 +449,7 @@ def edit(project_url): url=project.url, summary=project.summary, description=project.description, - is_private=u'GET' not in project.permissions.world, + is_private='GET' not in project.permissions.world, category=project.category, status=project.status, ) @@ -471,7 +471,7 @@ def edit(project_url): if form.is_private.data: project.permissions.world = [] else: - project.permissions.world = [u'GET'] + project.permissions.world = ['GET'] project.update(api=api) # Reattach the pictures @@ -721,7 +721,7 @@ def project_update_nodes_list(node, project_id=None, list_name='latest'): elif not project_id: return None project_id = node.project - if type(project_id) is not unicode: + if type(project_id) is not str: project_id = node.project._id api = system_util.pillar_api() project = Project.find(project_id, api=api) diff --git a/pillar/web/redirects/__init__.py b/pillar/web/redirects/__init__.py index c4afba1f..22200f53 100644 --- a/pillar/web/redirects/__init__.py +++ b/pillar/web/redirects/__init__.py @@ -1,6 +1,6 @@ import logging import string -import urlparse +import urllib.parse from flask import Blueprint, redirect, current_app from werkzeug.exceptions import NotFound @@ -54,7 +54,7 @@ def redirect_with_short_code(short_code): # Redirect to 'theatre' view for the node. url = url_for_node(node=node) - url = urlparse.urljoin(url, '?t') + url = urllib.parse.urljoin(url, '?t') log.debug('Found short code %s, redirecting to %s', short_code, url) return redirect(url, code=307) diff --git a/pillar/web/users/routes.py b/pillar/web/users/routes.py index badc50ed..c882395a 100644 --- a/pillar/web/users/routes.py +++ b/pillar/web/users/routes.py @@ -2,7 +2,7 @@ import json import logging import httplib2 # used by the oauth2 package import requests -import urlparse +import urllib.parse from flask import (abort, Blueprint, current_app, flash, redirect, render_template, request, session, url_for) @@ -274,15 +274,15 @@ def user_roles_update(user_id): groups = set(user.groups or []) if grant_subscriber: - roles.add(u'subscriber') + roles.add('subscriber') groups.add(group_subscriber._id) - elif u'admin' not in roles: + elif 'admin' not in roles: # Don't take away roles from admins. - roles.discard(u'subscriber') + roles.discard('subscriber') groups.discard(group_subscriber._id) if grant_demo: - roles.add(u'demo') + roles.add('demo') groups.add(group_demo._id) # Only send an API request when the user has actually changed @@ -316,7 +316,7 @@ def fetch_blenderid_user(): :rtype: dict """ - bid_url = urlparse.urljoin(current_app.config['BLENDER_ID_ENDPOINT'], 'api/user') + bid_url = urllib.parse.urljoin(current_app.config['BLENDER_ID_ENDPOINT'], 'api/user') log.debug('Fetching user info from %s', bid_url) try: diff --git a/pillar/web/utils/__init__.py b/pillar/web/utils/__init__.py index db5d2766..7848ce8b 100644 --- a/pillar/web/utils/__init__.py +++ b/pillar/web/utils/__init__.py @@ -1,6 +1,6 @@ import datetime import hashlib -import urllib +import urllib.request, urllib.parse, urllib.error import logging import traceback import sys @@ -52,7 +52,7 @@ def gravatar(email, size=64): parameters = {'s': str(size), 'd': 'mm'} return "https://www.gravatar.com/avatar/" + \ hashlib.md5(str(email)).hexdigest() + \ - "?" + urllib.urlencode(parameters) + "?" + urllib.parse.urlencode(parameters) def datetime_now(): @@ -73,7 +73,7 @@ def pretty_date(time, detail=False, now=None): # Normalize the 'time' parameter so it's always a datetime. if type(time) is int: time = datetime.datetime.fromtimestamp(time, tz=pillarsdk.utils.utc) - elif isinstance(time, basestring): + elif isinstance(time, str): time = dateutil.parser.parse(time) now = now or datetime.datetime.now(tz=time.tzinfo) @@ -184,10 +184,10 @@ def is_valid_id(some_id): :rtype: bool """ - if not isinstance(some_id, basestring): + if not isinstance(some_id, str): return False - if isinstance(some_id, unicode): + if isinstance(some_id, str): try: some_id = some_id.encode('ascii') except UnicodeEncodeError: diff --git a/pillar/web/utils/forms.py b/pillar/web/utils/forms.py index cbbc2fba..c7dd85b5 100644 --- a/pillar/web/utils/forms.py +++ b/pillar/web/utils/forms.py @@ -32,7 +32,7 @@ class CustomFileSelectWidget(HiddenInput): if file_format and file_format == 'image': file_format_regex = '^image\/(gif|jpe?g|png|tif?f|tga)$' - button = [u'
'] + button = ['
'] if field.data: api = system_util.pillar_api() @@ -42,64 +42,64 @@ class CustomFileSelectWidget(HiddenInput): except ResourceNotFound: pass else: - button.append(u'
') + button.append('
') filename = Markup.escape(file_item.filename) if file_item.content_type.split('/')[0] == 'image': # If a file of type image is available, display the preview - button.append(u''.format( + button.append(''.format( file_item.thumbnail('s', api=api))) - button.append(u'
    ') + button.append('
      ') # File name - button.append(u'
    • {0}
    • '.format(filename)) + button.append('
    • {0}
    • '.format(filename)) # File size - button.append(u'
    • ({0} MB)
    • '.format( + button.append('
    • ({0} MB)
    • '.format( round((file_item.length / 1024) * 0.001, 2))) # Image resolution (if image) if file_item.content_type.split('/')[0] == 'image': - button.append(u'
    • {0}x{1}
    • '.format( + button.append('
    • {0}x{1}
    • '.format( file_item.width, file_item.height)) - button.append(u'
    ') - button.append(u'
      ') + button.append('
    ') + button.append('
      ') # Download button for original file - button.append(u'
    • ' - u' ' - u'Original
    • ' + button.append('
    • ' + ' ' + 'Original
    • ' .format(file_item.link)) # Delete button - button.append(u'
    • ' - u' ' - u' Delete
    • '.format( + button.append('
    • ' + ' ' + ' Delete
    • '.format( field_name=field.name, file_id=field.data)) - button.append(u'
    ') - button.append(u'
') + button.append('') + button.append('
') - upload_url = u'%sstorage/stream/{project_id}' % current_app.config[ + upload_url = '%sstorage/stream/{project_id}' % current_app.config[ 'PILLAR_SERVER_ENDPOINT'] - button.append(u'' - u'
' - u'
' - u'
' - u'
'.format(url=upload_url, + button.append('' + '
' + '
' + '
' + '
'.format(url=upload_url, name=field.name, slug=field.name.replace('oid', 'slug'), token=Markup.escape(current_user.id), file_format=Markup.escape(file_format_regex))) - button.append(u'
') + button.append('
') - return HTMLString(html + u''.join(button)) + return HTMLString(html + ''.join(button)) class FileSelectField(StringField): @@ -112,7 +112,7 @@ def build_file_select_form(schema): class FileSelectForm(Form): pass - for field_name, field_schema in schema.iteritems(): + for field_name, field_schema in schema.items(): if field_schema['type'] == 'boolean': field = BooleanField() elif field_schema['type'] == 'string': @@ -142,19 +142,19 @@ class CustomFormFieldWidget(object): def __call__(self, field, **kwargs): html = [] kwargs.setdefault('id', field.id) - html.append(u'
' % html_params(**kwargs)) - hidden = u'' + html.append('
' % html_params(**kwargs)) + hidden = '' for subfield in field: if subfield.type == 'HiddenField': hidden += text_type(subfield) else: - html.append(u'
%s%s%s
' % ( + html.append('
%s%s%s
' % ( text_type(subfield.label), hidden, text_type(subfield))) - hidden = u'' - html.append(u'
') + hidden = '' + html.append('
') if hidden: html.append(hidden) - return HTMLString(u''.join(html)) + return HTMLString(''.join(html)) class CustomFormField(FormField): From b65dd49aa62f551d45a48500ff4249159fdbbe37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:01:38 +0100 Subject: [PATCH 05/31] Renamed static.py to staticfile.py Python 3 supports 'namespace packages', and thus can see a directory without __init__.py as something importable. This caused a name conflict, since there were both the file static.py and the dir static. --- pillar/__init__.py | 2 +- pillar/web/{static.py => staticfile.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pillar/web/{static.py => staticfile.py} (100%) diff --git a/pillar/__init__.py b/pillar/__init__.py index 2e136a34..a1b2c2e6 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -280,7 +280,7 @@ class PillarServer(Eve): ext.static_path) def register_static_file_endpoint(self, url_prefix, endpoint_name, static_folder): - from pillar.web.static import PillarStaticFile + from pillar.web.staticfile import PillarStaticFile view_func = PillarStaticFile.as_view(endpoint_name, static_folder=static_folder) self.add_url_rule('%s/' % url_prefix, view_func=view_func) diff --git a/pillar/web/static.py b/pillar/web/staticfile.py similarity index 100% rename from pillar/web/static.py rename to pillar/web/staticfile.py From b454b011b005bffc1be45e3e0dea276a717f5d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:07:11 +0100 Subject: [PATCH 06/31] Ran 2to3 on unittests, same sort of manual fixups as before --- tests/test_api/test_auth.py | 90 ++--- tests/test_api/test_bcloud_home_project.py | 38 +- tests/test_api/test_cli.py | 450 ++++++++++----------- tests/test_api/test_file_caching.py | 4 +- tests/test_api/test_file_storage.py | 16 +- tests/test_api/test_file_storage_moving.py | 18 +- tests/test_api/test_link_refresh.py | 14 +- tests/test_api/test_nodes.py | 16 +- tests/test_api/test_nodes_moving.py | 2 - tests/test_api/test_patch.py | 26 +- tests/test_api/test_project_management.py | 82 ++-- tests/test_api/test_service_badger.py | 10 +- tests/test_api/test_utils.py | 28 +- tests/test_sdk.py | 4 +- tests/test_web/test_user_admin.py | 8 +- tests/test_web/test_utils.py | 12 +- 16 files changed, 405 insertions(+), 413 deletions(-) diff --git a/tests/test_api/test_auth.py b/tests/test_api/test_auth.py index fa216052..b18ead39 100644 --- a/tests/test_api/test_auth.py +++ b/tests/test_api/test_auth.py @@ -19,15 +19,15 @@ EXAMPLE_PROJECT = copy.deepcopy(ctd.EXAMPLE_PROJECT) _texture_nt = next(nt for nt in EXAMPLE_PROJECT['node_types'] if nt['name'] == 'texture') _texture_nt['permissions'] = {'groups': [ - {u'group': ObjectId('5596e975ea893b269af85c0f'), u'methods': [u'GET']}, - {u'group': ObjectId('564733b56dcaf85da2faee8a'), u'methods': [u'GET']} + {'group': ObjectId('5596e975ea893b269af85c0f'), 'methods': ['GET']}, + {'group': ObjectId('564733b56dcaf85da2faee8a'), 'methods': ['GET']} ]} _asset_nt = next(nt for nt in EXAMPLE_PROJECT['node_types'] if nt['name'] == 'asset') _asset_nt['permissions'] = {'groups': [ - {u'group': ObjectId('5596e975ea893b269af85c0f'), u'methods': [u'DELETE', u'GET']}, - {u'group': ObjectId('564733b56dcaf85da2faee8a'), u'methods': [u'GET']} + {'group': ObjectId('5596e975ea893b269af85c0f'), 'methods': ['DELETE', 'GET']}, + {'group': ObjectId('564733b56dcaf85da2faee8a'), 'methods': ['GET']} ]} @@ -127,7 +127,7 @@ class AuthenticationTests(AbstractPillarTest): from pillar.api.utils import authentication as auth from pillar.api.utils import PillarJSONEncoder, remove_private_keys - user_id = self.create_user(roles=[u'subscriber']) + user_id = self.create_user(roles=['subscriber']) now = datetime.datetime.now(tz_util.utc) future = now + datetime.timedelta(days=1) @@ -174,7 +174,7 @@ class AuthenticationTests(AbstractPillarTest): users = self.app.data.driver.db['users'] db_user = users.find_one(user_id) - self.assertEqual([u'subscriber'], db_user['roles']) + self.assertEqual(['subscriber'], db_user['roles']) def test_token_expiry(self): """Expired tokens should be deleted from the database.""" @@ -207,9 +207,9 @@ class UserListTests(AbstractPillarTest): def setUp(self, **kwargs): super(UserListTests, self).setUp() - self.create_user(roles=[u'subscriber'], user_id='123456789abc123456789abc') - self.create_user(roles=[u'admin'], user_id='223456789abc123456789abc') - self.create_user(roles=[u'subscriber'], user_id='323456789abc123456789abc') + self.create_user(roles=['subscriber'], user_id='123456789abc123456789abc') + self.create_user(roles=['admin'], user_id='223456789abc123456789abc') + self.create_user(roles=['subscriber'], user_id='323456789abc123456789abc') self.create_valid_auth_token('123456789abc123456789abc', 'token') self.create_valid_auth_token('223456789abc123456789abc', 'admin-token') @@ -371,7 +371,7 @@ class UserListTests(AbstractPillarTest): 'roles': ['subscriber'], 'settings': {'email_communications': 1}, 'auth': [], - 'full_name': u'คนรักของผัดไทย', + 'full_name': 'คนรักของผัดไทย', 'email': TEST_EMAIL_ADDRESS, } @@ -464,22 +464,22 @@ class PermissionComputationTest(AbstractPillarTest): # Test project permissions. self.assertEqual( { - u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), - u'methods': [u'DELETE', u'GET', u'POST', u'PUT']}], - u'world': [u'GET'] + 'groups': [{'group': ObjectId('5596e975ea893b269af85c0e'), + 'methods': ['DELETE', 'GET', 'POST', 'PUT']}], + 'world': ['GET'] }, self.sort(compute_aggr_permissions('projects', EXAMPLE_PROJECT, None))) # Test node type permissions. self.assertEqual( { - u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), - u'methods': [u'DELETE', u'GET', u'POST', u'PUT']}, - {u'group': ObjectId('5596e975ea893b269af85c0f'), - u'methods': [u'GET']}, - {u'group': ObjectId('564733b56dcaf85da2faee8a'), - u'methods': [u'GET']}], - u'world': [u'GET'] + 'groups': [{'group': ObjectId('5596e975ea893b269af85c0e'), + 'methods': ['DELETE', 'GET', 'POST', 'PUT']}, + {'group': ObjectId('5596e975ea893b269af85c0f'), + 'methods': ['GET']}, + {'group': ObjectId('564733b56dcaf85da2faee8a'), + 'methods': ['GET']}], + 'world': ['GET'] }, self.sort(compute_aggr_permissions('projects', EXAMPLE_PROJECT, 'texture'))) @@ -492,13 +492,13 @@ class PermissionComputationTest(AbstractPillarTest): # Test node permissions without embedded project. self.ensure_project_exists(project_overrides=EXAMPLE_PROJECT) self.assertEqual( - {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), - u'methods': [u'DELETE', u'GET', u'POST', u'PUT']}, - {u'group': ObjectId('5596e975ea893b269af85c0f'), - u'methods': [u'DELETE', u'GET']}, - {u'group': ObjectId('564733b56dcaf85da2faee8a'), - u'methods': [u'GET']}], - u'world': [u'GET']}, + {'groups': [{'group': ObjectId('5596e975ea893b269af85c0e'), + 'methods': ['DELETE', 'GET', 'POST', 'PUT']}, + {'group': ObjectId('5596e975ea893b269af85c0f'), + 'methods': ['DELETE', 'GET']}, + {'group': ObjectId('564733b56dcaf85da2faee8a'), + 'methods': ['GET']}], + 'world': ['GET']}, self.sort(compute_aggr_permissions('nodes', node, None))) with self.app.test_request_context(): @@ -506,13 +506,13 @@ class PermissionComputationTest(AbstractPillarTest): node = copy.deepcopy(EXAMPLE_NODE) node['project'] = EXAMPLE_PROJECT self.assertEqual( - {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), - u'methods': [u'DELETE', u'GET', u'POST', u'PUT']}, - {u'group': ObjectId('5596e975ea893b269af85c0f'), - u'methods': [u'DELETE', u'GET']}, - {u'group': ObjectId('564733b56dcaf85da2faee8a'), - u'methods': [u'GET']}], - u'world': [u'GET']}, + {'groups': [{'group': ObjectId('5596e975ea893b269af85c0e'), + 'methods': ['DELETE', 'GET', 'POST', 'PUT']}, + {'group': ObjectId('5596e975ea893b269af85c0f'), + 'methods': ['DELETE', 'GET']}, + {'group': ObjectId('564733b56dcaf85da2faee8a'), + 'methods': ['GET']}], + 'world': ['GET']}, self.sort(compute_aggr_permissions('nodes', node, None))) @@ -529,7 +529,7 @@ class RequireRolesTest(AbstractPillarTest): with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'succubus']} + 'roles': ['succubus']} call_me() self.assertTrue(called[0]) @@ -540,19 +540,19 @@ class RequireRolesTest(AbstractPillarTest): called = [False] - @require_login(require_roles={u'admin'}) + @require_login(require_roles={'admin'}) def call_me(): called[0] = True with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'succubus']} + 'roles': ['succubus']} self.assertRaises(Forbidden, call_me) self.assertFalse(called[0]) with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'admin']} + 'roles': ['admin']} call_me() self.assertTrue(called[0]) @@ -562,32 +562,32 @@ class RequireRolesTest(AbstractPillarTest): called = [False] - @require_login(require_roles={u'service', u'badger'}, + @require_login(require_roles={'service', 'badger'}, require_all=True) def call_me(): called[0] = True with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'admin']} + 'roles': ['admin']} self.assertRaises(Forbidden, call_me) self.assertFalse(called[0]) with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'service']} + 'roles': ['service']} self.assertRaises(Forbidden, call_me) self.assertFalse(called[0]) with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'badger']} + 'roles': ['badger']} self.assertRaises(Forbidden, call_me) self.assertFalse(called[0]) with self.app.test_request_context(): g.current_user = {'user_id': ObjectId(24 * 'a'), - 'roles': [u'service', u'badger']} + 'roles': ['service', 'badger']} call_me() self.assertTrue(called[0]) @@ -596,8 +596,8 @@ class RequireRolesTest(AbstractPillarTest): with self.app.test_request_context(): self.assertTrue(user_has_role('subscriber', {'roles': ['aap', 'noot', 'subscriber']})) - self.assertTrue(user_has_role('subscriber', {'roles': [u'aap', u'subscriber']})) - self.assertFalse(user_has_role('admin', {'roles': [u'aap', u'noot', u'subscriber']})) + self.assertTrue(user_has_role('subscriber', {'roles': ['aap', 'subscriber']})) + self.assertFalse(user_has_role('admin', {'roles': ['aap', 'noot', 'subscriber']})) self.assertFalse(user_has_role('admin', {'roles': []})) self.assertFalse(user_has_role('admin', {'roles': None})) self.assertFalse(user_has_role('admin', {})) diff --git a/tests/test_api/test_bcloud_home_project.py b/tests/test_api/test_bcloud_home_project.py index bb89ec94..90532d3e 100644 --- a/tests/test_api/test_bcloud_home_project.py +++ b/tests/test_api/test_bcloud_home_project.py @@ -24,7 +24,7 @@ class AbstractHomeProjectTest(AbstractPillarTest): Adds the 'homeproject' role too, which we need to get past the AB-testing. """ - user_id = self.create_user(roles=roles.union({u'homeproject'}), user_id=user_id) + user_id = self.create_user(roles=roles.union({'homeproject'}), user_id=user_id) self.create_valid_auth_token(user_id, token) return user_id @@ -34,7 +34,7 @@ class HomeProjectTest(AbstractHomeProjectTest): from pillar.api.blender_cloud import home_project from pillar.api.utils.authentication import validate_token - user_id = self._create_user_with_token(roles={u'subscriber'}, token='token') + user_id = self._create_user_with_token(roles={'subscriber'}, token='token') # Test home project creation with self.app.test_request_context(headers={'Authorization': self.make_header('token')}): @@ -42,7 +42,7 @@ class HomeProjectTest(AbstractHomeProjectTest): proj = home_project.create_home_project(user_id, write_access=True) self.assertEqual('home', proj['category']) - self.assertEqual({u'group', u'asset', u'comment'}, + self.assertEqual({'group', 'asset', 'comment'}, set(nt['name'] for nt in proj['node_types'])) endpoint = url_for('blender_cloud.home_project.home_project') @@ -143,11 +143,11 @@ class HomeProjectTest(AbstractHomeProjectTest): admin_group_id = json_proj['permissions']['groups'][0]['group'] # Check that a Blender Sync node was created automatically. - expected_node_permissions = {u'users': [], - u'groups': [ - {u'group': ObjectId(admin_group_id), - u'methods': [u'GET', u'PUT', u'POST', u'DELETE']}, ], - u'world': []} + expected_node_permissions = {'users': [], + 'groups': [ + {'group': ObjectId(admin_group_id), + 'methods': ['GET', 'PUT', 'POST', 'DELETE']}, ], + 'world': []} with self.app.test_request_context(headers={'Authorization': self.make_header('token')}): nodes_coll = self.app.data.driver.db['nodes'] node = nodes_coll.find_one({ @@ -191,7 +191,7 @@ class HomeProjectTest(AbstractHomeProjectTest): from pillar.api.blender_cloud import home_project from pillar.api.utils.authentication import validate_token - user_id = self._create_user_with_token(roles={u'subscriber'}, token='token') + user_id = self._create_user_with_token(roles={'subscriber'}, token='token') # Test home project creation with self.app.test_request_context(headers={'Authorization': self.make_header('token')}): @@ -257,8 +257,8 @@ class HomeProjectTest(AbstractHomeProjectTest): from pillar.api.blender_cloud import home_project from pillar.api.utils.authentication import validate_token - uid1 = self._create_user_with_token(roles={u'subscriber'}, token='token1', user_id=24 * 'a') - uid2 = self._create_user_with_token(roles={u'subscriber'}, token='token2', user_id=24 * 'b') + uid1 = self._create_user_with_token(roles={'subscriber'}, token='token1', user_id=24 * 'a') + uid2 = self._create_user_with_token(roles={'subscriber'}, token='token2', user_id=24 * 'b') # Create home projects with self.app.test_request_context(headers={'Authorization': self.make_header('token1')}): @@ -292,7 +292,7 @@ class HomeProjectTest(AbstractHomeProjectTest): def test_delete_restore(self): """Deleting and then recreating a home project should restore the deleted project.""" - self._create_user_with_token(roles={u'subscriber'}, token='token') + self._create_user_with_token(roles={'subscriber'}, token='token') # Create home project by getting it. resp = self.client.get('/api/bcloud/home-project', @@ -460,8 +460,8 @@ class TextureLibraryTest(AbstractHomeProjectTest): libs = resp.json()['_items'] library_project_ids = {proj['_id'] for proj in libs} - self.assertNotIn(unicode(self.hdri_proj_id), library_project_ids) - self.assertIn(unicode(self.tex_proj_id), library_project_ids) + self.assertNotIn(str(self.hdri_proj_id), library_project_ids) + self.assertIn(str(self.tex_proj_id), library_project_ids) def test_hdri_library__old_bcloud_addon(self): resp = self.get('/api/bcloud/texture-libraries', @@ -469,8 +469,8 @@ class TextureLibraryTest(AbstractHomeProjectTest): headers={'Blender-Cloud-Addon': '1.3.3'}) libs = resp.json()['_items'] library_project_ids = {proj['_id'] for proj in libs} - self.assertNotIn(unicode(self.hdri_proj_id), library_project_ids) - self.assertIn(unicode(self.tex_proj_id), library_project_ids) + self.assertNotIn(str(self.hdri_proj_id), library_project_ids) + self.assertIn(str(self.tex_proj_id), library_project_ids) def test_hdri_library__new_bcloud_addon(self): resp = self.get('/api/bcloud/texture-libraries', @@ -478,8 +478,8 @@ class TextureLibraryTest(AbstractHomeProjectTest): headers={'Blender-Cloud-Addon': '1.4.0'}) libs = resp.json()['_items'] library_project_ids = {proj['_id'] for proj in libs} - self.assertIn(unicode(self.hdri_proj_id), library_project_ids) - self.assertIn(unicode(self.tex_proj_id), library_project_ids) + self.assertIn(str(self.hdri_proj_id), library_project_ids) + self.assertIn(str(self.tex_proj_id), library_project_ids) class HdriSortingTest(AbstractHomeProjectTest): @@ -488,7 +488,7 @@ class HdriSortingTest(AbstractHomeProjectTest): super(HdriSortingTest, self).setUp(**kwargs) - self.user_id = self._create_user_with_token({u'subscriber'}, 'token') + self.user_id = self._create_user_with_token({'subscriber'}, 'token') self.hdri_proj_id, proj = self.ensure_project_exists(project_overrides={ 'user': self.user_id, 'permissions': {'world': ['DELETE', 'GET', 'POST', 'PUT']}, diff --git a/tests/test_api/test_cli.py b/tests/test_api/test_cli.py index 0326a014..b4f0cd88 100644 --- a/tests/test_api/test_cli.py +++ b/tests/test_api/test_cli.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import absolute_import + import datetime from bson import tz_util, ObjectId @@ -11,202 +11,202 @@ from pillar.api.projects.utils import get_node_type EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID = ObjectId('5673541534134154134513c3') EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA = { - u'_created': datetime.datetime(2015, 12, 17, 13, 22, 56, tzinfo=tz_util.utc), - u'_etag': u'cc4643e98d3606f87bbfaaa200bfbae941b642f3', - u'_id': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, - u'_updated': datetime.datetime(2016, 1, 7, 18, 59, 4, tzinfo=tz_util.utc), - u'category': u'assets', - u'description': u'Welcome to this curated collection of Blender Institute textures and image ' - u'resources. This collection is an on-going project, as with each project we ' - u'create a number of textures based on our own resources (photographs, scans, ' - u'etc.) or made completely from scratch. At the moment you can find all the ' - u'textures from the past Open Projects that were deemed re-usable. \r\n\r\n' - u'People who have contributed to these textures:\r\n\r\nAndrea Weikert, Andy ' - u'Goralczyk, Basse Salmela, Ben Dansie, Campbell Barton, Enrico Valenza, Ian ' - u'Hubert, Kjartan Tysdal, Manu J\xe4rvinen, Massimiliana Pulieso, Matt Ebb, ' - u'Pablo Vazquez, Rob Tuytel, Roland Hess, Sarah Feldlaufer, S\xf6nke M\xe4ter', - u'is_private': False, - u'name': u'Unittest project', - u'node_types': [ - {u'description': u'Group for texture node type', - u'dyn_schema': {u'order': {u'type': u'integer'}, - u'status': {u'allowed': [u'published', u'pending'], - u'type': u'string'}, - u'url': {u'type': u'string'}}, - u'form_schema': {}, - u'name': u'group_texture', - u'parent': [u'group_texture', u'project']}, - {u'description': u'Folder node', - u'dyn_schema': {u'notes': {u'maxlength': 256, u'type': u'string'}, - u'order': {u'type': u'integer'}, - u'status': {u'allowed': [u'published', u'pending'], - u'type': u'string'}, - u'url': {u'type': u'string'}}, - u'form_schema': {}, - u'name': u'group', - u'parent': [u'group', u'project']}, - {u'description': u'Basic Asset Type', - u'dyn_schema': { - u'attachments': {u'schema': {u'schema': {u'field': {u'type': u'string'}, - u'files': {u'schema': { - u'schema': {u'file': { - u'data_relation': { - u'embeddable': True, - u'field': u'_id', - u'resource': u'files'}, - u'type': u'objectid'}, - u'size': { - u'type': u'string'}, - u'slug': { - u'minlength': 1, - u'type': u'string'}}, - u'type': u'dict'}, - u'type': u'list'}}, - u'type': u'dict'}, - u'type': u'list'}, - u'categories': {u'type': u'string'}, - u'content_type': {u'type': u'string'}, - u'file': {u'data_relation': {u'embeddable': True, - u'field': u'_id', - u'resource': u'files'}, - u'type': u'objectid'}, - u'order': {u'type': u'integer'}, - u'status': {u'allowed': [u'published', - u'pending', - u'processing'], - u'type': u'string'}, - u'tags': {u'schema': {u'type': u'string'}, u'type': u'list'}}, - u'form_schema': {u'attachments': {u'visible': False}, - u'content_type': {u'visible': False}, - u'file': {u'visible': False}}, - u'name': u'asset', - u'parent': [u'group']}, - {u'description': u'Entrypoint to a remote or local storage solution', - u'dyn_schema': {u'backend': {u'type': u'string'}, - u'subdir': {u'type': u'string'}}, - u'form_schema': {u'backend': {}, u'subdir': {}}, - u'name': u'storage', - u'parent': [u'group', u'project'], - u'permissions': {u'groups': [{u'group': ctd.EXAMPLE_ADMIN_GROUP_ID, - u'methods': [u'GET', u'PUT', u'POST']}, - {u'group': ctd.EXAMPLE_PROJECT_READONLY_GROUP_ID, - u'methods': [u'GET']}, - {u'group': ctd.EXAMPLE_PROJECT_READONLY_GROUP2_ID, - u'methods': [u'GET']}], - u'users': [], - u'world': []}}, - {u'description': u'Comments for asset nodes, pages, etc.', - u'dyn_schema': {u'confidence': {u'type': u'float'}, - u'content': {u'minlength': 5, u'type': u'string'}, - u'is_reply': {u'type': u'boolean'}, - u'rating_negative': {u'type': u'integer'}, - u'rating_positive': {u'type': u'integer'}, - u'ratings': {u'schema': { - u'schema': {u'is_positive': {u'type': u'boolean'}, - u'user': {u'type': u'objectid'}, - u'weight': {u'type': u'integer'}}, - u'type': u'dict'}, - u'type': u'list'}, - u'status': {u'allowed': [u'published', u'flagged', u'edited'], - u'type': u'string'}}, - u'form_schema': {}, - u'name': u'comment', - u'parent': [u'asset', u'comment']}, - {u'description': u'Container for node_type post.', - u'dyn_schema': {u'categories': {u'schema': {u'type': u'string'}, - u'type': u'list'}, - u'template': {u'type': u'string'}}, - u'form_schema': {}, - u'name': u'blog', - u'parent': [u'project']}, - {u'description': u'A blog post, for any project', - u'dyn_schema': { - u'attachments': {u'schema': {u'schema': {u'field': {u'type': u'string'}, - u'files': {u'schema': { - u'schema': {u'file': { - u'data_relation': { - u'embeddable': True, - u'field': u'_id', - u'resource': u'files'}, - u'type': u'objectid'}, - u'size': { - u'type': u'string'}, - u'slug': { - u'minlength': 1, - u'type': u'string'}}, - u'type': u'dict'}, - u'type': u'list'}}, - u'type': u'dict'}, - u'type': u'list'}, - u'category': {u'type': u'string'}, - u'content': {u'maxlength': 90000, - u'minlength': 5, - u'required': True, - u'type': u'string'}, - u'status': {u'allowed': [u'published', u'pending'], - u'default': u'pending', - u'type': u'string'}, - u'url': {u'type': u'string'}}, - u'form_schema': {u'attachments': {u'visible': False}}, - u'name': u'post', - u'parent': [u'blog']}, - {u'description': u'Image Texture', - u'dyn_schema': {u'aspect_ratio': {u'type': u'float'}, - u'categories': {u'type': u'string'}, - u'files': {u'schema': {u'schema': { - u'file': {u'data_relation': {u'embeddable': True, - u'field': u'_id', - u'resource': u'files'}, - u'type': u'objectid'}, - u'is_tileable': {u'type': u'boolean'}, - u'map_type': {u'allowed': [u'color', - u'specular', - u'bump', - u'normal', - u'translucency', - u'emission', - u'alpha'], - u'type': u'string'}}, - u'type': u'dict'}, - u'type': u'list'}, - u'is_landscape': {u'type': u'boolean'}, - u'is_tileable': {u'type': u'boolean'}, - u'order': {u'type': u'integer'}, - u'resolution': {u'type': u'string'}, - u'status': {u'allowed': [u'published', - u'pending', - u'processing'], - u'type': u'string'}, - u'tags': {u'schema': {u'type': u'string'}, u'type': u'list'}}, - u'form_schema': {u'content_type': {u'visible': False}, - u'files': {u'visible': False}}, - u'name': u'texture', - u'parent': [u'group']}], - u'nodes_blog': [], - u'nodes_featured': [], - u'nodes_latest': [], - u'permissions': {u'groups': [{u'group': ctd.EXAMPLE_ADMIN_GROUP_ID, - u'methods': [u'GET', u'POST', u'PUT', u'DELETE']}], - u'users': [], - u'world': [u'GET']}, - u'status': u'published', - u'summary': u'Texture collection from all Blender Institute open projects.', - u'url': u'attachment-schema-update', - u'picture_header': ObjectId('5673f260c379cf0007b31bc4'), - u'picture_square': ObjectId('5673f256c379cf0007b31bc3'), - u'user': ctd.EXAMPLE_PROJECT_OWNER_ID} + '_created': datetime.datetime(2015, 12, 17, 13, 22, 56, tzinfo=tz_util.utc), + '_etag': 'cc4643e98d3606f87bbfaaa200bfbae941b642f3', + '_id': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, + '_updated': datetime.datetime(2016, 1, 7, 18, 59, 4, tzinfo=tz_util.utc), + 'category': 'assets', + 'description': 'Welcome to this curated collection of Blender Institute textures and image ' + 'resources. This collection is an on-going project, as with each project we ' + 'create a number of textures based on our own resources (photographs, scans, ' + 'etc.) or made completely from scratch. At the moment you can find all the ' + 'textures from the past Open Projects that were deemed re-usable. \r\n\r\n' + 'People who have contributed to these textures:\r\n\r\nAndrea Weikert, Andy ' + 'Goralczyk, Basse Salmela, Ben Dansie, Campbell Barton, Enrico Valenza, Ian ' + 'Hubert, Kjartan Tysdal, Manu J\xe4rvinen, Massimiliana Pulieso, Matt Ebb, ' + 'Pablo Vazquez, Rob Tuytel, Roland Hess, Sarah Feldlaufer, S\xf6nke M\xe4ter', + 'is_private': False, + 'name': 'Unittest project', + 'node_types': [ + {'description': 'Group for texture node type', + 'dyn_schema': {'order': {'type': 'integer'}, + 'status': {'allowed': ['published', 'pending'], + 'type': 'string'}, + 'url': {'type': 'string'}}, + 'form_schema': {}, + 'name': 'group_texture', + 'parent': ['group_texture', 'project']}, + {'description': 'Folder node', + 'dyn_schema': {'notes': {'maxlength': 256, 'type': 'string'}, + 'order': {'type': 'integer'}, + 'status': {'allowed': ['published', 'pending'], + 'type': 'string'}, + 'url': {'type': 'string'}}, + 'form_schema': {}, + 'name': 'group', + 'parent': ['group', 'project']}, + {'description': 'Basic Asset Type', + 'dyn_schema': { + 'attachments': {'schema': {'schema': {'field': {'type': 'string'}, + 'files': {'schema': { + 'schema': {'file': { + 'data_relation': { + 'embeddable': True, + 'field': '_id', + 'resource': 'files'}, + 'type': 'objectid'}, + 'size': { + 'type': 'string'}, + 'slug': { + 'minlength': 1, + 'type': 'string'}}, + 'type': 'dict'}, + 'type': 'list'}}, + 'type': 'dict'}, + 'type': 'list'}, + 'categories': {'type': 'string'}, + 'content_type': {'type': 'string'}, + 'file': {'data_relation': {'embeddable': True, + 'field': '_id', + 'resource': 'files'}, + 'type': 'objectid'}, + 'order': {'type': 'integer'}, + 'status': {'allowed': ['published', + 'pending', + 'processing'], + 'type': 'string'}, + 'tags': {'schema': {'type': 'string'}, 'type': 'list'}}, + 'form_schema': {'attachments': {'visible': False}, + 'content_type': {'visible': False}, + 'file': {'visible': False}}, + 'name': 'asset', + 'parent': ['group']}, + {'description': 'Entrypoint to a remote or local storage solution', + 'dyn_schema': {'backend': {'type': 'string'}, + 'subdir': {'type': 'string'}}, + 'form_schema': {'backend': {}, 'subdir': {}}, + 'name': 'storage', + 'parent': ['group', 'project'], + 'permissions': {'groups': [{'group': ctd.EXAMPLE_ADMIN_GROUP_ID, + 'methods': ['GET', 'PUT', 'POST']}, + {'group': ctd.EXAMPLE_PROJECT_READONLY_GROUP_ID, + 'methods': ['GET']}, + {'group': ctd.EXAMPLE_PROJECT_READONLY_GROUP2_ID, + 'methods': ['GET']}], + 'users': [], + 'world': []}}, + {'description': 'Comments for asset nodes, pages, etc.', + 'dyn_schema': {'confidence': {'type': 'float'}, + 'content': {'minlength': 5, 'type': 'string'}, + 'is_reply': {'type': 'boolean'}, + 'rating_negative': {'type': 'integer'}, + 'rating_positive': {'type': 'integer'}, + 'ratings': {'schema': { + 'schema': {'is_positive': {'type': 'boolean'}, + 'user': {'type': 'objectid'}, + 'weight': {'type': 'integer'}}, + 'type': 'dict'}, + 'type': 'list'}, + 'status': {'allowed': ['published', 'flagged', 'edited'], + 'type': 'string'}}, + 'form_schema': {}, + 'name': 'comment', + 'parent': ['asset', 'comment']}, + {'description': 'Container for node_type post.', + 'dyn_schema': {'categories': {'schema': {'type': 'string'}, + 'type': 'list'}, + 'template': {'type': 'string'}}, + 'form_schema': {}, + 'name': 'blog', + 'parent': ['project']}, + {'description': 'A blog post, for any project', + 'dyn_schema': { + 'attachments': {'schema': {'schema': {'field': {'type': 'string'}, + 'files': {'schema': { + 'schema': {'file': { + 'data_relation': { + 'embeddable': True, + 'field': '_id', + 'resource': 'files'}, + 'type': 'objectid'}, + 'size': { + 'type': 'string'}, + 'slug': { + 'minlength': 1, + 'type': 'string'}}, + 'type': 'dict'}, + 'type': 'list'}}, + 'type': 'dict'}, + 'type': 'list'}, + 'category': {'type': 'string'}, + 'content': {'maxlength': 90000, + 'minlength': 5, + 'required': True, + 'type': 'string'}, + 'status': {'allowed': ['published', 'pending'], + 'default': 'pending', + 'type': 'string'}, + 'url': {'type': 'string'}}, + 'form_schema': {'attachments': {'visible': False}}, + 'name': 'post', + 'parent': ['blog']}, + {'description': 'Image Texture', + 'dyn_schema': {'aspect_ratio': {'type': 'float'}, + 'categories': {'type': 'string'}, + 'files': {'schema': {'schema': { + 'file': {'data_relation': {'embeddable': True, + 'field': '_id', + 'resource': 'files'}, + 'type': 'objectid'}, + 'is_tileable': {'type': 'boolean'}, + 'map_type': {'allowed': ['color', + 'specular', + 'bump', + 'normal', + 'translucency', + 'emission', + 'alpha'], + 'type': 'string'}}, + 'type': 'dict'}, + 'type': 'list'}, + 'is_landscape': {'type': 'boolean'}, + 'is_tileable': {'type': 'boolean'}, + 'order': {'type': 'integer'}, + 'resolution': {'type': 'string'}, + 'status': {'allowed': ['published', + 'pending', + 'processing'], + 'type': 'string'}, + 'tags': {'schema': {'type': 'string'}, 'type': 'list'}}, + 'form_schema': {'content_type': {'visible': False}, + 'files': {'visible': False}}, + 'name': 'texture', + 'parent': ['group']}], + 'nodes_blog': [], + 'nodes_featured': [], + 'nodes_latest': [], + 'permissions': {'groups': [{'group': ctd.EXAMPLE_ADMIN_GROUP_ID, + 'methods': ['GET', 'POST', 'PUT', 'DELETE']}], + 'users': [], + 'world': ['GET']}, + 'status': 'published', + 'summary': 'Texture collection from all Blender Institute open projects.', + 'url': 'attachment-schema-update', + 'picture_header': ObjectId('5673f260c379cf0007b31bc4'), + 'picture_square': ObjectId('5673f256c379cf0007b31bc3'), + 'user': ctd.EXAMPLE_PROJECT_OWNER_ID} EXAMPLE_ASSET_NODE_OLD_ATTACHMENT_SCHEMA = { - u'_id': ObjectId('572761099837730efe8e120d'), - u'picture': ObjectId('5673f260c379cf0007b31bc4'), - u'description': u'', - u'node_type': u'asset', - u'user': ctd.EXAMPLE_PROJECT_OWNER_ID, - u'properties': { - u'status': u'published', - u'content_type': u'image', - u'file': ObjectId('5673f260c379cf0007b31bed'), - u'attachments': [{ + '_id': ObjectId('572761099837730efe8e120d'), + 'picture': ObjectId('5673f260c379cf0007b31bc4'), + 'description': '', + 'node_type': 'asset', + 'user': ctd.EXAMPLE_PROJECT_OWNER_ID, + 'properties': { + 'status': 'published', + 'content_type': 'image', + 'file': ObjectId('5673f260c379cf0007b31bed'), + 'attachments': [{ 'files': [ {'slug': '01', 'file': ObjectId('5679b25ec379cf25636688f6')}, {'slug': '02b', 'file': ObjectId('5679b308c379cf25636688f7')}, @@ -215,24 +215,24 @@ EXAMPLE_ASSET_NODE_OLD_ATTACHMENT_SCHEMA = { 'field': 'properties.content' }], }, - u'_updated': datetime.datetime(2016, 5, 2, 14, 19, 58, 0, tzinfo=tz_util.utc), - u'name': u'Image test', - u'project': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, - u'_created': datetime.datetime(2016, 5, 2, 14, 19, 37, 0, tzinfo=tz_util.utc), - u'_etag': u'6b8589b42c880e3626f43f3e82a5c5b946742687' + '_updated': datetime.datetime(2016, 5, 2, 14, 19, 58, 0, tzinfo=tz_util.utc), + 'name': 'Image test', + 'project': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, + '_created': datetime.datetime(2016, 5, 2, 14, 19, 37, 0, tzinfo=tz_util.utc), + '_etag': '6b8589b42c880e3626f43f3e82a5c5b946742687' } EXAMPLE_PAGE_NODE_OLD_ATTACHMENT_SCHEMA = { - u'_id': ObjectId('572761099837730efe8e120a'), - u'picture': ObjectId('5673f260c379cf0007b31bc4'), - u'description': u'', - u'node_type': u'page', - u'user': ctd.EXAMPLE_PROJECT_OWNER_ID, - u'properties': { - u'status': u'published', - u'content': u'Überinteressant Verhaaltje™ voor het slapengaan.', - u'url': u'jemoeder', - u'attachments': [{ + '_id': ObjectId('572761099837730efe8e120a'), + 'picture': ObjectId('5673f260c379cf0007b31bc4'), + 'description': '', + 'node_type': 'page', + 'user': ctd.EXAMPLE_PROJECT_OWNER_ID, + 'properties': { + 'status': 'published', + 'content': 'Überinteressant Verhaaltje™ voor het slapengaan.', + 'url': 'jemoeder', + 'attachments': [{ 'files': [ {'slug': '03', 'file': ObjectId('5679b33bc379cf256366ddd8')}, {'slug': '04', 'file': ObjectId('5679b35bc379cf256366ddd9')}, @@ -240,11 +240,11 @@ EXAMPLE_PAGE_NODE_OLD_ATTACHMENT_SCHEMA = { 'field': 'properties.content' }], }, - u'_updated': datetime.datetime(2016, 5, 2, 14, 19, 58, 0, tzinfo=tz_util.utc), - u'name': u'Page test', - u'project': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, - u'_created': datetime.datetime(2016, 5, 2, 14, 19, 37, 0, tzinfo=tz_util.utc), - u'_etag': u'6b8589b42c880e3626f43f3e82a5c5b946742687' + '_updated': datetime.datetime(2016, 5, 2, 14, 19, 58, 0, tzinfo=tz_util.utc), + 'name': 'Page test', + 'project': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, + '_created': datetime.datetime(2016, 5, 2, 14, 19, 37, 0, tzinfo=tz_util.utc), + '_etag': '6b8589b42c880e3626f43f3e82a5c5b946742687' } @@ -258,11 +258,11 @@ class AbstractNodeReplacementTest(AbstractPillarTest): project_overrides=self.project_overrides) self.ensure_file_exists({ - '_id': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA[u'picture_header'], + '_id': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA['picture_header'], 'project': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, }) self.ensure_file_exists({ - '_id': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA[u'picture_square'], + '_id': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA['picture_square'], 'project': EXAMPLE_PROJECT_OLD_ATTACHMENT_SCHEMA_ID, }) @@ -270,8 +270,8 @@ class AbstractNodeReplacementTest(AbstractPillarTest): return super(AbstractNodeReplacementTest, self).fetch_project_from_db(self.project_id) def add_group_permission_to_asset_node_type(self): - group_perms = {u'group': ctd.EXAMPLE_PROJECT_READONLY_GROUP_ID, - u'methods': [u'POST', u'PUT']} + group_perms = {'group': ctd.EXAMPLE_PROJECT_READONLY_GROUP_ID, + 'methods': ['POST', 'PUT']} # Assign some permissions to the node types, so we're sure they don't get overwritten. with self.app.app_context(): proj_coll = self.app.db()['projects'] @@ -314,13 +314,13 @@ class UpgradeAttachmentSchemaTest(AbstractNodeReplacementTest): super(UpgradeAttachmentSchemaTest, self).setUp(**kwargs) self.ensure_file_exists( - {'_id': EXAMPLE_ASSET_NODE_OLD_ATTACHMENT_SCHEMA[u'properties'][u'file']}) + {'_id': EXAMPLE_ASSET_NODE_OLD_ATTACHMENT_SCHEMA['properties']['file']}) for node in (EXAMPLE_ASSET_NODE_OLD_ATTACHMENT_SCHEMA, EXAMPLE_PAGE_NODE_OLD_ATTACHMENT_SCHEMA): - for att in node[u'properties'][u'attachments']: - for filedict in att[u'files']: - self.ensure_file_exists({'_id': filedict[u'file']}) + for att in node['properties']['attachments']: + for filedict in att['files']: + self.ensure_file_exists({'_id': filedict['file']}) def test_schema_upgrade(self): from pillar.cli import upgrade_attachment_schema @@ -348,8 +348,8 @@ class CreateBlogTest(AbstractPillarTest): AbstractPillarTest.setUp(self, **kwargs) self.project_id, self.proj = self.ensure_project_exists() - self.ensure_file_exists({'_id': self.proj[u'picture_header']}) - self.ensure_file_exists({'_id': self.proj[u'picture_square']}) + self.ensure_file_exists({'_id': self.proj['picture_header']}) + self.ensure_file_exists({'_id': self.proj['picture_square']}) def test_create_blog(self): """Very simple test to check the create_blog CLI command.""" diff --git a/tests/test_api/test_file_caching.py b/tests/test_api/test_file_caching.py index ca9d37fc..573b42b7 100644 --- a/tests/test_api/test_file_caching.py +++ b/tests/test_api/test_file_caching.py @@ -25,7 +25,7 @@ class FileCachingTest(AbstractPillarTest): # Make sure the file link has not expired. expires = datetime.datetime.now(tz=bson.tz_util.utc) + datetime.timedelta(minutes=1) file_id, file_doc = self.ensure_file_exists(file_overrides={ - u'link_expires': expires + 'link_expires': expires }) updated = file_doc['_updated'].strftime(RFC1123_DATE_FORMAT) @@ -49,7 +49,7 @@ class FileCachingTest(AbstractPillarTest): # Make sure the file link has expired. expires = datetime.datetime.now(tz=bson.tz_util.utc) - datetime.timedelta(seconds=1) file_id, file_doc = self.ensure_file_exists(file_overrides={ - u'link_expires': expires + 'link_expires': expires }) updated = file_doc['_updated'].strftime(RFC1123_DATE_FORMAT) diff --git a/tests/test_api/test_file_storage.py b/tests/test_api/test_file_storage.py index 7de38aad..74a48fc3 100644 --- a/tests/test_api/test_file_storage.py +++ b/tests/test_api/test_file_storage.py @@ -75,8 +75,8 @@ class FileAccessTest(AbstractPillarTest): img_file_id, _ = self.ensure_file_exists() video_file_id, _ = self.ensure_file_exists({ - u'_id': None, - u'content_type': u'video/matroska', + '_id': None, + 'content_type': 'video/matroska', 'variations': [ { 'format': 'mp4', @@ -105,14 +105,14 @@ class FileAccessTest(AbstractPillarTest): ] }) - blend_file_id, _ = self.ensure_file_exists({u'_id': None, - u'content_type': u'application/x-blender', - u'variations': None}) + blend_file_id, _ = self.ensure_file_exists({'_id': None, + 'content_type': 'application/x-blender', + 'variations': None}) nonsub_user_id = self.create_user(user_id='cafef00dcafef00d00000000', roles=()) - sub_user_id = self.create_user(user_id='cafef00dcafef00dcafef00d', roles=(u'subscriber',)) - demo_user_id = self.create_user(user_id='cafef00dcafef00ddeadbeef', roles=(u'demo',)) - admin_user_id = self.create_user(user_id='aaaaaaaaaaaaaaaaaaaaaaaa', roles=(u'admin',)) + sub_user_id = self.create_user(user_id='cafef00dcafef00dcafef00d', roles=('subscriber',)) + demo_user_id = self.create_user(user_id='cafef00dcafef00ddeadbeef', roles=('demo',)) + admin_user_id = self.create_user(user_id='aaaaaaaaaaaaaaaaaaaaaaaa', roles=('admin',)) self.create_valid_auth_token(nonsub_user_id, 'nonsub-token') self.create_valid_auth_token(sub_user_id, 'sub-token') diff --git a/tests/test_api/test_file_storage_moving.py b/tests/test_api/test_file_storage_moving.py index 3cf8a7ea..f553fc6f 100644 --- a/tests/test_api/test_file_storage_moving.py +++ b/tests/test_api/test_file_storage_moving.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import datetime import responses @@ -47,11 +45,11 @@ class ChangeBackendTest(AbstractPillarTest): files_coll = self.app.data.driver.db['files'] fdoc = files_coll.find_one(image_file_id) - self.assertEqual(u'gcs', fdoc['backend']) - self.assertIn(u'/path/to/testing/gcs/', fdoc['link']) + self.assertEqual('gcs', fdoc['backend']) + self.assertIn('/path/to/testing/gcs/', fdoc['link']) for variation in fdoc['variations']: - self.assertIn(u'/path/to/testing/gcs/', variation['link']) + self.assertIn('/path/to/testing/gcs/', variation['link']) @mock.activate def test_only_variations(self): @@ -77,11 +75,11 @@ class ChangeBackendTest(AbstractPillarTest): files_coll = self.app.data.driver.db['files'] fdoc = files_coll.find_one(image_file_id) - self.assertEqual(u'gcs', fdoc['backend']) - self.assertIn(u'/path/to/testing/gcs/', fdoc['link']) + self.assertEqual('gcs', fdoc['backend']) + self.assertIn('/path/to/testing/gcs/', fdoc['link']) for variation in fdoc['variations']: - self.assertIn(u'/path/to/testing/gcs/', variation['link']) + self.assertIn('/path/to/testing/gcs/', variation['link']) @mock.activate def test_no_variations(self): @@ -102,8 +100,8 @@ class ChangeBackendTest(AbstractPillarTest): files_coll = self.app.data.driver.db['files'] fdoc = files_coll.find_one(image_file_id) - self.assertEqual(u'gcs', fdoc['backend']) - self.assertIn(u'/path/to/testing/gcs/', fdoc['link']) + self.assertEqual('gcs', fdoc['backend']) + self.assertIn('/path/to/testing/gcs/', fdoc['link']) def _create_image_file_doc(self, variations=True): fdoc = {'status': 'complete', 'name': 'some-hash.jpg', 'backend': 'unittest', diff --git a/tests/test_api/test_link_refresh.py b/tests/test_api/test_link_refresh.py index 7c8ce65a..558c7723 100644 --- a/tests/test_api/test_link_refresh.py +++ b/tests/test_api/test_link_refresh.py @@ -9,13 +9,13 @@ from datetime import datetime, timedelta class LinkRefreshTest(AbstractPillarTest): # noinspection PyMethodOverriding def ensure_file_exists(self, file_overrides): - file_id = file_overrides[u'_id'] + file_id = file_overrides['_id'] file_overrides.update({ - u'_id': ObjectId(file_id), - u'name': '%s.png' % file_id, - u'file_path': '%s.png' % file_id, - u'backend': 'unittest', + '_id': ObjectId(file_id), + 'name': '%s.png' % file_id, + 'file_path': '%s.png' % file_id, + 'backend': 'unittest', }) return super(LinkRefreshTest, self).ensure_file_exists(file_overrides) @@ -33,8 +33,8 @@ class LinkRefreshTest(AbstractPillarTest): datetime(2016, 3, 22, 9, 28, 3, tzinfo=tz_util.utc), self.now + timedelta(minutes=30), self.now + timedelta(minutes=90), ] ids_and_files = [self.ensure_file_exists(file_overrides={ - u'_id': 'cafef00ddeadbeef0000000%i' % file_idx, - u'link_expires': expiry}) + '_id': 'cafef00ddeadbeef0000000%i' % file_idx, + 'link_expires': expiry}) for file_idx, expiry in enumerate(expiry)] self.file_id, self.file = zip(*ids_and_files) diff --git a/tests/test_api/test_nodes.py b/tests/test_api/test_nodes.py index 6168a390..26f5f063 100644 --- a/tests/test_api/test_nodes.py +++ b/tests/test_api/test_nodes.py @@ -43,7 +43,7 @@ class NodeContentTypeTest(AbstractPillarTest): g.current_user = {'user_id': user_id, # This group is hardcoded in the EXAMPLE_PROJECT. 'groups': [ObjectId('5596e975ea893b269af85c0e')], - 'roles': {u'subscriber', u'admin'}} + 'roles': {'subscriber', 'admin'}} nodes = self.app.data.driver.db['nodes'] # Create the node. @@ -81,7 +81,7 @@ class NodeContentTypeTest(AbstractPillarTest): self.assertEqual(200, resp.status_code) data = json.loads(resp.data) - self.assertEqual([u'GET'], data['allowed_methods']) + self.assertEqual(['GET'], data['allowed_methods']) def test_default_picture_image_asset(self): from pillar.api.utils import dumps @@ -240,12 +240,12 @@ class NodeSharingTest(AbstractPillarTest): self.project_id, _ = self.ensure_project_exists( project_overrides={ - u'category': 'home', - u'permissions': - {u'groups': [{u'group': ctd.EXAMPLE_ADMIN_GROUP_ID, - u'methods': [u'GET', u'POST', u'PUT', u'DELETE']}], - u'users': [], - u'world': []}} + 'category': 'home', + 'permissions': + {'groups': [{'group': ctd.EXAMPLE_ADMIN_GROUP_ID, + 'methods': ['GET', 'POST', 'PUT', 'DELETE']}], + 'users': [], + 'world': []}} ) self.user_id = self.create_user(groups=[ctd.EXAMPLE_ADMIN_GROUP_ID]) self.create_valid_auth_token(self.user_id, 'token') diff --git a/tests/test_api/test_nodes_moving.py b/tests/test_api/test_nodes_moving.py index 40fadea0..9f735a01 100644 --- a/tests/test_api/test_nodes_moving.py +++ b/tests/test_api/test_nodes_moving.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import unittest import mock diff --git a/tests/test_api/test_patch.py b/tests/test_api/test_patch.py index 6b2b8493..01b93652 100644 --- a/tests/test_api/test_patch.py +++ b/tests/test_api/test_patch.py @@ -60,7 +60,7 @@ class VoteCommentTest(AbstractPatchCommentTest): patched_node = self.get(self.node_url, auth_token='token').json() self.assertEqual(1, patched_node['properties']['rating_positive']) self.assertEqual(0, patched_node['properties']['rating_negative']) - self.assertEqual({u'user': str(self.user_id), u'is_positive': True}, + self.assertEqual({'user': str(self.user_id), 'is_positive': True}, patched_node['properties']['ratings'][0]) self.assertEqual(1, len(patched_node['properties']['ratings'])) @@ -81,7 +81,7 @@ class VoteCommentTest(AbstractPatchCommentTest): patched_node = self.get(self.node_url, auth_token='token').json() self.assertEqual(0, patched_node['properties']['rating_positive']) self.assertEqual(1, patched_node['properties']['rating_negative']) - self.assertEqual({u'user': str(self.user_id), u'is_positive': False}, + self.assertEqual({'user': str(self.user_id), 'is_positive': False}, patched_node['properties']['ratings'][0]) self.assertEqual(1, len(patched_node['properties']['ratings'])) @@ -157,11 +157,11 @@ class VoteCommentTest(AbstractPatchCommentTest): self.assertEqual(3, patched_node['properties']['rating_positive']) self.assertEqual(2, patched_node['properties']['rating_negative']) self.assertEqual([ - {u'user': unicode(other_user_ids[0]), u'is_positive': True}, - {u'user': unicode(other_user_ids[1]), u'is_positive': True}, - {u'user': unicode(other_user_ids[3]), u'is_positive': True}, - {u'user': unicode(other_user_ids[4]), u'is_positive': False}, - {u'user': unicode(self.user_id), u'is_positive': False}, + {'user': str(other_user_ids[0]), 'is_positive': True}, + {'user': str(other_user_ids[1]), 'is_positive': True}, + {'user': str(other_user_ids[3]), 'is_positive': True}, + {'user': str(other_user_ids[4]), 'is_positive': False}, + {'user': str(self.user_id), 'is_positive': False}, ], patched_node['properties'].get('ratings', [])) @@ -172,19 +172,19 @@ class EditCommentTest(AbstractPatchCommentTest): res = self.patch(self.node_url, json={'op': 'edit', 'content': 'Je moeder is niet je vader.'}, auth_token=token).json() - self.assertEqual(u'

Je moeder is niet je vader.

\n', + self.assertEqual('

Je moeder is niet je vader.

\n', res['properties']['content_html']) # Get the node again, to inspect its changed state. patched_node = self.get(self.node_url, auth_token=token).json() - self.assertEqual(u'Je moeder is niet je vader.', + self.assertEqual('Je moeder is niet je vader.', patched_node['properties']['content']) - self.assertEqual(u'

Je moeder is niet je vader.

\n', + self.assertEqual('

Je moeder is niet je vader.

\n', patched_node['properties']['content_html']) self.assertNotEqual(pre_node['_etag'], patched_node['_etag']) def test_comment_edit_other_user_admin(self): - admin_id = self.create_user(user_id=24 * 'c', roles={u'admin'}) + admin_id = self.create_user(user_id=24 * 'c', roles={'admin'}) self.create_valid_auth_token(admin_id, 'admin-token') self.test_comment_edit_happy(token='admin-token') @@ -197,9 +197,9 @@ class EditCommentTest(AbstractPatchCommentTest): # Get the node again, to inspect its old state. patched_node = self.get(self.node_url, auth_token='token').json() - self.assertEqual(u'Purrrr kittycat', + self.assertEqual('Purrrr kittycat', patched_node['properties']['content']) - self.assertEqual(u'

Purrrr kittycat

\n', + self.assertEqual('

Purrrr kittycat

\n', patched_node['properties']['content_html']) def test_edit_noncomment_node(self): diff --git a/tests/test_api/test_project_management.py b/tests/test_api/test_project_management.py index c0c637f1..389783cb 100644 --- a/tests/test_api/test_project_management.py +++ b/tests/test_api/test_project_management.py @@ -5,7 +5,7 @@ import functools import json import logging -import urllib +import urllib.request, urllib.parse, urllib.error from bson import ObjectId from pillar.tests import AbstractPillarTest @@ -26,7 +26,7 @@ class AbstractProjectTest(AbstractPillarTest): return resp def _create_user_and_project(self, roles, user_id='cafef00df00df00df00df00d', token='token', - project_name=u'Prøject El Niño'): + project_name='Prøject El Niño'): self._create_user_with_token(roles, token, user_id=user_id) resp = self._create_project(project_name, token) @@ -38,8 +38,8 @@ class AbstractProjectTest(AbstractPillarTest): class ProjectCreationTest(AbstractProjectTest): def test_project_creation_wrong_role(self): - self._create_user_with_token([u'whatever'], 'token') - resp = self._create_project(u'Prøject El Niño', 'token') + self._create_user_with_token(['whatever'], 'token') + resp = self._create_project('Prøject El Niño', 'token') self.assertEqual(403, resp.status_code) @@ -49,8 +49,8 @@ class ProjectCreationTest(AbstractProjectTest): self.assertEqual(0, len(list(projects.find()))) def test_project_creation_good_role(self): - user_id = self._create_user_with_token([u'subscriber'], 'token') - resp = self._create_project(u'Prøject El Niño', 'token') + user_id = self._create_user_with_token(['subscriber'], 'token') + resp = self._create_project('Prøject El Niño', 'token') self.assertEqual(201, resp.status_code) # The response of a POST is the entire project, but we'll test a GET on @@ -69,7 +69,7 @@ class ProjectCreationTest(AbstractProjectTest): project_id = project['_id'] # Check some of the more complex/interesting fields. - self.assertEqual(u'Prøject El Niño', project['name']) + self.assertEqual('Prøject El Niño', project['name']) self.assertEqual(str(user_id), project['user']) self.assertEqual('p-%s' % project_id, project['url']) self.assertEqual(1, len(project['permissions']['groups'])) @@ -95,13 +95,13 @@ class ProjectCreationTest(AbstractProjectTest): def test_project_creation_access_admin(self): """Admin-created projects should be public""" - proj = self._create_user_and_project(roles={u'admin'}) + proj = self._create_user_and_project(roles={'admin'}) self.assertEqual(['GET'], proj['permissions']['world']) def test_project_creation_access_subscriber(self): """Subscriber-created projects should be private""" - proj = self._create_user_and_project(roles={u'subscriber'}) + proj = self._create_user_and_project(roles={'subscriber'}) self.assertEqual([], proj['permissions']['world']) self.assertTrue(proj['is_private']) @@ -116,12 +116,12 @@ class ProjectCreationTest(AbstractProjectTest): """Test that we get an empty list when querying for non-existing projects, instead of 403""" proj_a = self._create_user_and_project(user_id=24 * 'a', - roles={u'subscriber'}, - project_name=u'Prøject A', + roles={'subscriber'}, + project_name='Prøject A', token='token-a') proj_b = self._create_user_and_project(user_id=24 * 'b', - roles={u'subscriber'}, - project_name=u'Prøject B', + roles={'subscriber'}, + project_name='Prøject B', token='token-b') # Assertion: each user must have access to their own project. @@ -137,16 +137,16 @@ class ProjectCreationTest(AbstractProjectTest): headers={'Authorization': self.make_header('token-a')}) self.assertEqual(200, resp.status_code) proj_list = json.loads(resp.data) - self.assertEqual({u'Prøject A'}, {p['name'] for p in proj_list['_items']}) + self.assertEqual({'Prøject A'}, {p['name'] for p in proj_list['_items']}) resp = self.client.get('/api/projects', headers={'Authorization': self.make_header('token-b')}) self.assertEqual(200, resp.status_code) proj_list = json.loads(resp.data) - self.assertEqual({u'Prøject B'}, {p['name'] for p in proj_list['_items']}) + self.assertEqual({'Prøject B'}, {p['name'] for p in proj_list['_items']}) # No access to anything for user C, should result in empty list. - self._create_user_with_token(roles={u'subscriber'}, token='token-c', user_id=12 * 'c') + self._create_user_with_token(roles={'subscriber'}, token='token-c', user_id=12 * 'c') resp = self.client.get('/api/projects', headers={'Authorization': self.make_header('token-c')}) self.assertEqual(200, resp.status_code) @@ -161,7 +161,7 @@ class ProjectEditTest(AbstractProjectTest): from pillar.api.utils import remove_private_keys, PillarJSONEncoder dumps = functools.partial(json.dumps, cls=PillarJSONEncoder) - project_info = self._create_user_and_project([u'subscriber']) + project_info = self._create_user_and_project(['subscriber']) project_url = '/api/projects/%(_id)s' % project_info resp = self.client.get(project_url, @@ -180,11 +180,11 @@ class ProjectEditTest(AbstractProjectTest): # Regular user should be able to PUT, but only be able to edit certain fields. put_project = remove_private_keys(project) - put_project['url'] = u'very-offensive-url' - put_project['description'] = u'Blender je besplatan set alata za izradu interaktivnog 3D ' \ - u'sadržaja pod različitim operativnim sustavima.' - put_project['name'] = u'โครงการปั่นเมฆ' - put_project['summary'] = u'Это переведена на Google' + put_project['url'] = 'very-offensive-url' + put_project['description'] = 'Blender je besplatan set alata za izradu interaktivnog 3D ' \ + 'sadržaja pod različitim operativnim sustavima.' + put_project['name'] = 'โครงการปั่นเมฆ' + put_project['summary'] = 'Это переведена на Google' put_project['status'] = 'pending' put_project['category'] = 'software' put_project['user'] = other_user_id @@ -222,7 +222,7 @@ class ProjectEditTest(AbstractProjectTest): from pillar.api.utils import remove_private_keys, PillarJSONEncoder dumps = functools.partial(json.dumps, cls=PillarJSONEncoder) - project_info = self._create_user_and_project([u'subscriber', u'admin']) + project_info = self._create_user_and_project(['subscriber', 'admin']) project_url = '/api/projects/%(_id)s' % project_info resp = self.client.get(project_url) @@ -234,11 +234,11 @@ class ProjectEditTest(AbstractProjectTest): # Admin user should be able to PUT everything. put_project = remove_private_keys(project) - put_project['url'] = u'very-offensive-url' - put_project['description'] = u'Blender je besplatan set alata za izradu interaktivnog 3D ' \ - u'sadržaja pod različitim operativnim sustavima.' - put_project['name'] = u'โครงการปั่นเมฆ' - put_project['summary'] = u'Это переведена на Google' + put_project['url'] = 'very-offensive-url' + put_project['description'] = 'Blender je besplatan set alata za izradu interaktivnog 3D ' \ + 'sadržaja pod različitim operativnim sustavima.' + put_project['name'] = 'โครงการปั่นเมฆ' + put_project['summary'] = 'Это переведена на Google' put_project['is_private'] = False put_project['status'] = 'pending' put_project['category'] = 'software' @@ -272,7 +272,7 @@ class ProjectEditTest(AbstractProjectTest): dumps = functools.partial(json.dumps, cls=PillarJSONEncoder) # Create test project. - project = self._create_user_and_project([u'subscriber']) + project = self._create_user_and_project(['subscriber']) project_id = project['_id'] project_url = '/api/projects/%s' % project_id @@ -281,7 +281,7 @@ class ProjectEditTest(AbstractProjectTest): # Admin user should be able to PUT. put_project = remove_private_keys(project) - put_project['name'] = u'โครงการปั่นเมฆ' + put_project['name'] = 'โครงการปั่นเมฆ' resp = self.client.put(project_url, data=dumps(put_project), @@ -297,7 +297,7 @@ class ProjectEditTest(AbstractProjectTest): dumps = functools.partial(json.dumps, cls=PillarJSONEncoder) # Create test project. - project = self._create_user_and_project([u'subscriber']) + project = self._create_user_and_project(['subscriber']) project_id = project['_id'] project_url = '/api/projects/%s' % project_id @@ -307,7 +307,7 @@ class ProjectEditTest(AbstractProjectTest): # Regular subscriber should not be able to do this. put_project = remove_private_keys(project) - put_project['name'] = u'Болту́н -- нахо́дка для шпио́на.' + put_project['name'] = 'Болту́н -- нахо́дка для шпио́на.' put_project['user'] = my_user_id resp = self.client.put(project_url, data=dumps(put_project), @@ -318,7 +318,7 @@ class ProjectEditTest(AbstractProjectTest): def test_delete_by_admin(self): # Create public test project. - project_info = self._create_user_and_project([u'admin']) + project_info = self._create_user_and_project(['admin']) project_id = project_info['_id'] project_url = '/api/projects/%s' % project_id @@ -338,7 +338,7 @@ class ProjectEditTest(AbstractProjectTest): # ... but we should still get it in the body. db_proj = json.loads(resp.data) - self.assertEqual(u'Prøject El Niño', db_proj['name']) + self.assertEqual('Prøject El Niño', db_proj['name']) self.assertTrue(db_proj['_deleted']) # Querying for deleted projects should include it. @@ -347,16 +347,16 @@ class ProjectEditTest(AbstractProjectTest): projection = json.dumps({'name': 1, 'permissions': 1}) where = json.dumps({'_deleted': True}) # MUST be True, 1 does not work. resp = self.client.get('/api/projects?where=%s&projection=%s' % - (urllib.quote(where), urllib.quote(projection))) + (urllib.parse.quote(where), urllib.parse.quote(projection))) self.assertEqual(200, resp.status_code, resp.data) projlist = json.loads(resp.data) self.assertEqual(1, projlist['_meta']['total']) - self.assertEqual(u'Prøject El Niño', projlist['_items'][0]['name']) + self.assertEqual('Prøject El Niño', projlist['_items'][0]['name']) def test_delete_by_subscriber(self): # Create test project. - project_info = self._create_user_and_project([u'subscriber']) + project_info = self._create_user_and_project(['subscriber']) project_id = project_info['_id'] project_url = '/api/projects/%s' % project_id @@ -383,14 +383,14 @@ class ProjectNodeAccess(AbstractProjectTest): from pillar.api.utils import PillarJSONEncoder # Project is created by regular subscriber, so should be private. - self.user_id = self._create_user_with_token([u'subscriber'], 'token') - resp = self._create_project(u'Prøject El Niño', 'token') + self.user_id = self._create_user_with_token(['subscriber'], 'token') + resp = self._create_project('Prøject El Niño', 'token') self.assertEqual(201, resp.status_code) self.assertEqual('application/json', resp.mimetype) self.project = json.loads(resp.data) self.project_id = ObjectId(self.project['_id']) - self.other_user_id = self._create_user_with_token([u'subscriber'], 'other-token', + self.other_user_id = self._create_user_with_token(['subscriber'], 'other-token', user_id='deadbeefdeadbeefcafef00d') self.test_node = { @@ -435,7 +435,7 @@ class ProjectNodeAccess(AbstractProjectTest): headers={'Authorization': self.make_header('token')}) self.assertEqual(200, resp.status_code, (resp.status_code, resp.data)) listed_nodes = json.loads(resp.data)['_items'] - self.assertEquals(self.node_id, listed_nodes[0]['_id']) + self.assertEqual(self.node_id, listed_nodes[0]['_id']) # Listing all nodes should not include nodes from private projects. resp = self.client.get('/api/nodes', diff --git a/tests/test_api/test_service_badger.py b/tests/test_api/test_service_badger.py index aa4b5bcd..0592a9bd 100644 --- a/tests/test_api/test_service_badger.py +++ b/tests/test_api/test_service_badger.py @@ -11,8 +11,8 @@ class BadgerServiceTest(AbstractPillarTest): with self.app.test_request_context(): self.badger, token_doc = service.create_service_account( - 'serviceaccount@example.com', [u'badger'], - {u'badger': [u'succubus', u'subscriber', u'demo']} + 'serviceaccount@example.com', ['badger'], + {'badger': ['succubus', 'subscriber', 'demo']} ) self.badger_token = token_doc['token'] @@ -33,7 +33,7 @@ class BadgerServiceTest(AbstractPillarTest): with self.app.test_request_context(): user = self.app.data.driver.db['users'].find_one(self.user_id) - self.assertIn(u'succubus', user['roles']) + self.assertIn('succubus', user['roles']) # Aaaahhhw it's gone again resp = self._post({'action': 'revoke', 'user_email': self.user_email, 'role': 'succubus'}) @@ -41,7 +41,7 @@ class BadgerServiceTest(AbstractPillarTest): with self.app.test_request_context(): user = self.app.data.driver.db['users'].find_one(self.user_id) - self.assertNotIn(u'succubus', user['roles']) + self.assertNotIn('succubus', user['roles']) def test_grant_not_allowed_badge(self): resp = self._post({'action': 'grant', 'user_email': self.user_email, 'role': 'admin'}) @@ -49,7 +49,7 @@ class BadgerServiceTest(AbstractPillarTest): with self.app.test_request_context(): user = self.app.data.driver.db['users'].find_one(self.user_id) - self.assertNotIn(u'admin', user['roles']) + self.assertNotIn('admin', user['roles']) def test_group_membership(self): """Certain roles are linked to certain groups.""" diff --git a/tests/test_api/test_utils.py b/tests/test_api/test_utils.py index 70860bf2..8fcc9153 100644 --- a/tests/test_api/test_utils.py +++ b/tests/test_api/test_utils.py @@ -1,5 +1,5 @@ # -*- encoding: utf-8 -*- -from __future__ import absolute_import + import unittest from bson import ObjectId @@ -16,7 +16,7 @@ class Str2idTest(AbstractPillarTest): happy(24 * 'a') happy(12 * 'a') - happy(u'577e23ad98377323f74c368c') + happy('577e23ad98377323f74c368c') def test_unhappy(self): from pillar.api.utils import str2id @@ -25,10 +25,10 @@ class Str2idTest(AbstractPillarTest): self.assertRaises(BadRequest, str2id, str_id) unhappy(13 * 'a') - unhappy(u'577e23ad 8377323f74c368c') - unhappy(u'김치') # Kimchi + unhappy('577e23ad 8377323f74c368c') + unhappy('김치') # Kimchi + unhappy('') unhappy('') - unhappy(u'') unhappy(None) @@ -84,26 +84,26 @@ class DocDiffTest(unittest.TestCase): def test_no_diff_nested(self): from pillar.api.utils import doc_diff - diff = doc_diff({'a': 'b', 'props': {'status': u'todo', 'notes': u'jemoeder'}}, - {'a': 'b', 'props': {'status': u'todo', 'notes': u'jemoeder'}}) + diff = doc_diff({'a': 'b', 'props': {'status': 'todo', 'notes': 'jemoeder'}}, + {'a': 'b', 'props': {'status': 'todo', 'notes': 'jemoeder'}}) self.assertEqual([], list(diff)) def test_diff_values_nested(self): from pillar.api.utils import doc_diff - diff = doc_diff({'a': 'b', 'props': {'status': u'todo', 'notes': u'jemoeder'}}, - {'a': 'c', 'props': {'status': u'done', 'notes': u'jemoeder'}}) + diff = doc_diff({'a': 'b', 'props': {'status': 'todo', 'notes': 'jemoeder'}}, + {'a': 'c', 'props': {'status': 'done', 'notes': 'jemoeder'}}) - self.assertEqual({('a', 'b', 'c'), ('props.status', u'todo', u'done')}, + self.assertEqual({('a', 'b', 'c'), ('props.status', 'todo', 'done')}, set(diff)) def test_diff_keys_nested(self): from pillar.api.utils import doc_diff, DoesNotExist - diff = doc_diff({'a': 'b', 'props': {'status1': u'todo', 'notes': u'jemoeder'}}, - {'a': 'b', 'props': {'status2': u'todo', 'notes': u'jemoeder'}}) + diff = doc_diff({'a': 'b', 'props': {'status1': 'todo', 'notes': 'jemoeder'}}, + {'a': 'b', 'props': {'status2': 'todo', 'notes': 'jemoeder'}}) - self.assertEqual({('props.status1', u'todo', DoesNotExist), - ('props.status2', DoesNotExist, u'todo')}, + self.assertEqual({('props.status1', 'todo', DoesNotExist), + ('props.status2', DoesNotExist, 'todo')}, set(diff)) diff --git a/tests/test_sdk.py b/tests/test_sdk.py index 908ed096..e2768ec1 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -84,8 +84,8 @@ class FlaskInternalApiTest(AbstractPillarTest): with self.app.test_request_context(), open(blender_desktop_logo_path, 'rb') as fileobj: resp = pillarsdk.Node.create_asset_from_file( - unicode(self.project_id), - unicode(parent_id), + str(self.project_id), + str(parent_id), 'image', blender_desktop_logo_path, mimetype='image/jpeg', diff --git a/tests/test_web/test_user_admin.py b/tests/test_web/test_user_admin.py index 7e953e2e..46f01a3f 100644 --- a/tests/test_web/test_user_admin.py +++ b/tests/test_web/test_user_admin.py @@ -2,8 +2,6 @@ """Unit tests for the user admin interface.""" -from __future__ import absolute_import - import json import logging @@ -32,7 +30,7 @@ class UserAdminTest(AbstractPillarTest): import pillar.web.users.routes import pillar.auth - user_id = self.create_user(roles=(u'mønkeybütler', )) + user_id = self.create_user(roles=('mønkeybütler', )) self.create_valid_auth_token(user_id, 'token') # Try to access the home project, creating it. @@ -72,7 +70,7 @@ class UserAdminTest(AbstractPillarTest): dbuser = get_dbuser() self.assertEqual({home_project_gid, self.subscriber_gid, self.demo_gid}, set(dbuser['groups'])) - self.assertEqual({u'subscriber', u'demo', u'mønkeybütler'}, + self.assertEqual({'subscriber', 'demo', 'mønkeybütler'}, set(dbuser['roles'])) # Edit user again, revoking demo role. @@ -80,5 +78,5 @@ class UserAdminTest(AbstractPillarTest): dbuser = get_dbuser() self.assertEqual({home_project_gid, self.subscriber_gid}, set(dbuser['groups'])) - self.assertEqual({u'subscriber', u'mønkeybütler'}, + self.assertEqual({'subscriber', 'mønkeybütler'}, set(dbuser['roles'])) diff --git a/tests/test_web/test_utils.py b/tests/test_web/test_utils.py index 542c222e..ea1eff80 100644 --- a/tests/test_web/test_utils.py +++ b/tests/test_web/test_utils.py @@ -1,7 +1,5 @@ # -*- encoding: utf-8 -*- -from __future__ import absolute_import - import unittest import datetime @@ -13,20 +11,20 @@ class IsValidIdTest(unittest.TestCase): def test_valid(self): # 24-byte hex strings self.assertTrue(utils.is_valid_id(24 * 'a')) - self.assertTrue(utils.is_valid_id(24 * u'a')) + self.assertTrue(utils.is_valid_id(24 * 'a')) + self.assertTrue(utils.is_valid_id('deadbeefbeefcacedeadcace')) self.assertTrue(utils.is_valid_id('deadbeefbeefcacedeadcace')) - self.assertTrue(utils.is_valid_id(u'deadbeefbeefcacedeadcace')) # 12-byte arbitrary ASCII strings self.assertTrue(utils.is_valid_id('DeadBeefCake')) - self.assertTrue(utils.is_valid_id(u'DeadBeefCake')) + self.assertTrue(utils.is_valid_id('DeadBeefCake')) # 12-byte str object self.assertTrue(utils.is_valid_id('beef€67890')) def test_bad_length(self): self.assertFalse(utils.is_valid_id(23 * 'a')) - self.assertFalse(utils.is_valid_id(25 * u'a')) + self.assertFalse(utils.is_valid_id(25 * 'a')) def test_non_string(self): self.assertFalse(utils.is_valid_id(None)) @@ -38,7 +36,7 @@ class IsValidIdTest(unittest.TestCase): self.assertFalse(utils.is_valid_id('deadbeefbeefcakedeadcake')) # unicode variant of valid 12-byte str object - self.assertFalse(utils.is_valid_id(u'beef€67890')) + self.assertFalse(utils.is_valid_id('beef€67890')) class PrettyDateTest(unittest.TestCase): From 82157af84bb56fa266633390505c82ddc30cd8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:32:21 +0100 Subject: [PATCH 07/31] Removed no longer compatibility thingy for Python 3 --- pillar/api/utils/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index 452e6ef5..9eb397a1 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -148,11 +148,9 @@ def gravatar(email, size=64): "?" + urllib.parse.urlencode(parameters) - class MetaFalsey(type): def __bool__(cls): return False - __bool__ = __nonzero__ # for Python 3 class DoesNotExist(object, metaclass=MetaFalsey): From 72b002491a66fb291bbe031c3120dd7396170159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:33:01 +0100 Subject: [PATCH 08/31] Referring to Pillar Python SDK requirements This forces us to remove common requirements from Pillar's requirements.txt file (which is a good thing). --- requirements-dev.txt | 3 +++ requirements.txt | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c5f40f6..32a73467 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,12 @@ -r requirements.txt +-e ../pillar-python-sdk +-e . # Development requirements pytest==3.0.1 responses==0.5.1 pytest-cov==2.3.1 +mock==2.0.0 # Secondary development requirements cookies==2.2.1 diff --git a/requirements.txt b/requirements.txt index fe79316f..fea38733 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # Primary requirements -# pillarsdk +-r ../pillar-python-sdk/requirements.txt + attrs==16.2.0 algoliasearch==1.8.0 bcrypt==2.0.0 @@ -19,11 +20,9 @@ Flask-WTF==0.12 gcloud==0.12.0 google-apitools==0.4.11 httplib2==0.9.2 -idna==2.0 MarkupSafe==0.23 ndg-httpsclient==0.4.0 Pillow==2.8.1 -pycparser==2.14 pycrypto==2.6.1 pyOpenSSL==0.15.1 python-dateutil==2.5.3 @@ -39,9 +38,7 @@ Flask-PyMongo==0.4.1 Jinja2==2.8 Werkzeug==0.11.10 appdirs==1.4.2 -cffi==1.6.0 CommonMark==0.7.2 -cryptography==1.3.1 future==0.15.2 html5lib==0.9999999 googleapis-common-protos==1.1.0 @@ -51,7 +48,6 @@ oauthlib==2.0.1 packaging==16.8 protobuf==3.0.0b2.post2 protorpc==0.11.1 -pyasn1==0.1.9 pyasn1-modules==0.0.8 pymongo==3.2.2 pyparsing==2.1.10 From 60c608d095b8f7d63a13bf1e32e5b4e960e61ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:33:23 +0100 Subject: [PATCH 09/31] Fixup of syntax error introduced by 2to3 --- pillar/web/nodes/routes.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index f39f1a93..0c1a1fb0 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -583,7 +583,4 @@ def url_for_node(node_id=None, node=None): # Import of custom modules (using the same nodes decorator) -from . import custom.comments -from . import custom.groups -from . import custom.storage -from . import custom.posts +from .custom import comments, groups, storage, posts From 86b13557fb179ecd31fde624385b8b4dc1caa421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 12:33:48 +0100 Subject: [PATCH 10/31] Don't run failed unittests first. This can cause false positives when the failure was caused by inter-test interference. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 274157ea..3c19782f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -v --cov pillar --cov-report term-missing --ignore node_modules -x --ff +addopts = -v --cov pillar --cov-report term-missing --ignore node_modules -x [pep8] max-line-length = 100 From 6fb58a3f26023a7a2df899388597517379f9a5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 14:10:47 +0100 Subject: [PATCH 11/31] Python 3.6 compatibility: Prevent comparison with None --- pillar/api/blender_cloud/texture_libs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pillar/api/blender_cloud/texture_libs.py b/pillar/api/blender_cloud/texture_libs.py index ebf0b5a5..206ee8d9 100644 --- a/pillar/api/blender_cloud/texture_libs.py +++ b/pillar/api/blender_cloud/texture_libs.py @@ -74,7 +74,7 @@ def texture_libraries(): # of the Blender Cloud Addon. If the addon version is None, we're dealing # with a version of the BCA that's so old it doesn't send its version along. addon_version = blender_cloud_addon_version() - return_hdri = addon_version >= FIRST_ADDON_VERSION_WITH_HDRI + return_hdri = addon_version is not None and addon_version >= FIRST_ADDON_VERSION_WITH_HDRI log.debug('User %s has Blender Cloud Addon version %s; return_hdri=%s', current_user_id(), addon_version, return_hdri) From a9e40ccf107970811524575f29d6fee22dc15ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 14:14:36 +0100 Subject: [PATCH 12/31] Python 3.6 compatibility: bytes vs strings stuff These changes mostly revolve around the change in ObjectId constructor when running on Python 3.6. Where on 2.7 the constructor would accept 12- and 24-byte strings, now only 12-byte bytes and 24-character strings are accepted. Good thing, but required some changes in our code. Other changes include hashing of strings, which isn't supported, so they are converted to bytes first, and sometimes converted back afterwards. --- pillar/api/file_storage/__init__.py | 4 +-- pillar/api/local_auth.py | 20 +++++++++----- pillar/api/service.py | 2 +- pillar/api/utils/__init__.py | 2 +- pillar/api/utils/authentication.py | 2 +- pillar/tests/__init__.py | 5 ++-- pillar/web/utils/__init__.py | 32 ++++++++++------------- tests/test_api/test_local_auth.py | 11 ++++++++ tests/test_api/test_nodes_moving.py | 4 +-- tests/test_api/test_project_management.py | 11 ++++---- tests/test_api/test_utils.py | 5 ++-- tests/test_web/test_utils.py | 10 +++---- 12 files changed, 62 insertions(+), 46 deletions(-) diff --git a/pillar/api/file_storage/__init__.py b/pillar/api/file_storage/__init__.py index 81fbb750..0dbd5446 100644 --- a/pillar/api/file_storage/__init__.py +++ b/pillar/api/file_storage/__init__.py @@ -293,7 +293,7 @@ def delete_file(file_item): process_file_delete(file_item) -def generate_link(backend, file_path, project_id=None, is_public=False): +def generate_link(backend, file_path: str, project_id=None, is_public=False): """Hook to check the backend of a file resource, to build an appropriate link that can be used by the client to retrieve the actual file. """ @@ -321,7 +321,7 @@ def generate_link(backend, file_path, project_id=None, is_public=False): if backend == 'cdnsun': return hash_file_path(file_path, None) if backend == 'unittest': - return 'https://unit.test/%s' % md5(file_path).hexdigest() + return 'https://unit.test/%s' % md5(file_path.encode()).hexdigest() log.warning('generate_link(): Unknown backend %r, returning empty string as new link.', backend) diff --git a/pillar/api/local_auth.py b/pillar/api/local_auth.py index 0febed98..091c521b 100644 --- a/pillar/api/local_auth.py +++ b/pillar/api/local_auth.py @@ -1,6 +1,7 @@ import base64 import hashlib import logging +import typing import bcrypt import datetime @@ -67,12 +68,12 @@ def make_token(): return jsonify(token=token['token']) -def generate_and_store_token(user_id, days=15, prefix=''): +def generate_and_store_token(user_id, days=15, prefix=b''): """Generates token based on random bits. :param user_id: ObjectId of the owning user. :param days: token will expire in this many days. - :param prefix: the token will be prefixed by this string, for easy identification. + :param prefix: the token will be prefixed by these bytes, for easy identification. :return: the token document. """ @@ -80,17 +81,22 @@ def generate_and_store_token(user_id, days=15, prefix=''): # Use 'xy' as altargs to prevent + and / characters from appearing. # We never have to b64decode the string anyway. - token = prefix + base64.b64encode(random_bits, altchars='xy').strip('=') + token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=') token_expiry = datetime.datetime.now(tz=tz_util.utc) + datetime.timedelta(days=days) - return store_token(user_id, token, token_expiry) + return store_token(user_id, token.decode('ascii'), token_expiry) -def hash_password(password, salt): +def hash_password(password: str, salt: typing.Union[str, bytes]) -> str: + password = password.encode() + if isinstance(salt, str): salt = salt.encode('utf-8') - encoded_password = base64.b64encode(hashlib.sha256(password).digest()) - return bcrypt.hashpw(encoded_password, salt) + + hash = hashlib.sha256(password).digest() + encoded_password = base64.b64encode(hash) + hashed_password = bcrypt.hashpw(encoded_password, salt) + return hashed_password.decode('ascii') def setup_app(app, url_prefix): diff --git a/pillar/api/service.py b/pillar/api/service.py index ecd03a80..3e165a2d 100644 --- a/pillar/api/service.py +++ b/pillar/api/service.py @@ -221,7 +221,7 @@ def create_service_account(email, roles, service, update_existing=None): user.update(result) # Create an authentication token that won't expire for a long time. - token = local_auth.generate_and_store_token(user['_id'], days=36500, prefix='SRV') + token = local_auth.generate_and_store_token(user['_id'], days=36500, prefix=b'SRV') return user, token diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index 9eb397a1..77fdc19c 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -136,7 +136,7 @@ def str2id(document_id): try: return bson.ObjectId(document_id) - except bson.objectid.InvalidId: + except (bson.objectid.InvalidId, TypeError): log.debug('str2id(%r): Invalid Object ID', document_id) raise wz_exceptions.BadRequest('Invalid object ID %r' % document_id) diff --git a/pillar/api/utils/authentication.py b/pillar/api/utils/authentication.py index b58fd568..b1636423 100644 --- a/pillar/api/utils/authentication.py +++ b/pillar/api/utils/authentication.py @@ -118,7 +118,7 @@ def find_token(token, is_subclient_token=False, **extra_filters): return db_token -def store_token(user_id, token, token_expiry, oauth_subclient_id=False): +def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False): """Stores an authentication token. :returns: the token document from MongoDB diff --git a/pillar/tests/__init__.py b/pillar/tests/__init__.py index 9323c258..704f62bc 100644 --- a/pillar/tests/__init__.py +++ b/pillar/tests/__init__.py @@ -301,10 +301,11 @@ class AbstractPillarTest(TestMinimal): json=BLENDER_ID_USER_RESPONSE, status=200) - def make_header(self, username, subclient_id=''): + def make_header(self, username: str, subclient_id: str='') -> bytes: """Returns a Basic HTTP Authentication header value.""" - return 'basic ' + base64.b64encode('%s:%s' % (username, subclient_id)) + content = '%s:%s' % (username, subclient_id) + return b'basic ' + base64.b64encode(content.encode()) def create_standard_groups(self, additional_groups=()): """Creates standard admin/demo/subscriber groups, plus any additional. diff --git a/pillar/web/utils/__init__.py b/pillar/web/utils/__init__.py index 7848ce8b..67da2fb9 100644 --- a/pillar/web/utils/__init__.py +++ b/pillar/web/utils/__init__.py @@ -4,6 +4,7 @@ import urllib.request, urllib.parse, urllib.error import logging import traceback import sys +import typing import dateutil.parser from flask import current_app @@ -174,7 +175,7 @@ def get_main_project(): return main_project -def is_valid_id(some_id): +def is_valid_id(some_id: typing.Union[str, bytes]): """Returns True iff the given string is a valid ObjectId. Only use this if you do NOT need an ObjectId object. If you do need that, @@ -184,27 +185,22 @@ def is_valid_id(some_id): :rtype: bool """ + if isinstance(some_id, bytes): + return len(some_id) == 12 + if not isinstance(some_id, str): return False - if isinstance(some_id, str): - try: - some_id = some_id.encode('ascii') - except UnicodeEncodeError: - return False + if len(some_id) != 24: + return False - if len(some_id) == 12: - return True - elif len(some_id) == 24: - # This is more than 5x faster than checking character by - # character in a loop. - try: - int(some_id, 16) - except ValueError: - return False - return True - - return False + # This is more than 5x faster than checking character by + # character in a loop. + try: + int(some_id, 16) + except ValueError: + return False + return True def last_page_index(meta_info): diff --git a/tests/test_api/test_local_auth.py b/tests/test_api/test_local_auth.py index e2f3d274..5c3e8285 100644 --- a/tests/test_api/test_local_auth.py +++ b/tests/test_api/test_local_auth.py @@ -75,3 +75,14 @@ class LocalAuthTest(AbstractPillarTest): 'password': 'koro'}) self.assertEqual(403, resp.status_code, resp.data) + + def test_hash_password(self): + from pillar.api.local_auth import hash_password + + salt = b'$2b$12$cHdK4M8/yJ7SWp2Q.PYW0O' + self.assertEqual(hash_password('© 2017 je moeder™', salt), + '$2b$12$cHdK4M8/yJ7SWp2Q.PYW0OAU1gE3DIVdeehq0XIzOMM0Vp3ldPMb6') + self.assertIsInstance(hash_password('Резиновая уточка', salt), str) + + # The password should be encodable as ASCII. + hash_password('Резиновая уточка', salt).encode('ascii') diff --git a/tests/test_api/test_nodes_moving.py b/tests/test_api/test_nodes_moving.py index 9f735a01..246a2ce1 100644 --- a/tests/test_api/test_nodes_moving.py +++ b/tests/test_api/test_nodes_moving.py @@ -95,7 +95,7 @@ class NodeMoverTest(unittest.TestCase): ], } } - prid = ObjectId('project_dest') + prid = ObjectId(b'project_dest') new_project = { '_id': prid } @@ -124,7 +124,7 @@ class NodeMoverTest(unittest.TestCase): ], } } - prid = ObjectId('project_dest') + prid = ObjectId(b'project_dest') new_project = { '_id': prid } diff --git a/tests/test_api/test_project_management.py b/tests/test_api/test_project_management.py index 389783cb..09ba337d 100644 --- a/tests/test_api/test_project_management.py +++ b/tests/test_api/test_project_management.py @@ -146,7 +146,7 @@ class ProjectCreationTest(AbstractProjectTest): self.assertEqual({'Prøject B'}, {p['name'] for p in proj_list['_items']}) # No access to anything for user C, should result in empty list. - self._create_user_with_token(roles={'subscriber'}, token='token-c', user_id=12 * 'c') + self._create_user_with_token(roles={'subscriber'}, token='token-c', user_id=24 * 'c') resp = self.client.get('/api/projects', headers={'Authorization': self.make_header('token-c')}) self.assertEqual(200, resp.status_code) @@ -277,7 +277,7 @@ class ProjectEditTest(AbstractProjectTest): project_url = '/api/projects/%s' % project_id # Create test user. - self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeef') + self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeefcafef00dbeef') # Admin user should be able to PUT. put_project = remove_private_keys(project) @@ -324,7 +324,7 @@ class ProjectEditTest(AbstractProjectTest): # Create admin user that doesn't own the project, to check that # non-owner admins can delete projects too. - self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeef') + self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeefcafef00dbeef') # Admin user should be able to DELETE. resp = self.client.delete(project_url, @@ -361,7 +361,8 @@ class ProjectEditTest(AbstractProjectTest): project_url = '/api/projects/%s' % project_id # Create test user. - self._create_user_with_token(['subscriber'], 'mortal-token', user_id='cafef00dbeef') + self._create_user_with_token(['subscriber'], 'mortal-token', + user_id='cafef00dbeefcafef00dbeef') # Other user should NOT be able to DELETE. resp = self.client.delete(project_url, @@ -451,7 +452,7 @@ class ProjectNodeAccess(AbstractProjectTest): put_project = remove_private_keys(self.project) # Create admin user. - self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeef') + self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeefcafef00dbeef') # Make the project public put_project['permissions']['world'] = ['GET'] # make public diff --git a/tests/test_api/test_utils.py b/tests/test_api/test_utils.py index 8fcc9153..de1d350d 100644 --- a/tests/test_api/test_utils.py +++ b/tests/test_api/test_utils.py @@ -15,7 +15,7 @@ class Str2idTest(AbstractPillarTest): self.assertEqual(ObjectId(str_id), str2id(str_id)) happy(24 * 'a') - happy(12 * 'a') + happy(12 * b'a') happy('577e23ad98377323f74c368c') def test_unhappy(self): @@ -25,10 +25,11 @@ class Str2idTest(AbstractPillarTest): self.assertRaises(BadRequest, str2id, str_id) unhappy(13 * 'a') + unhappy(13 * b'a') unhappy('577e23ad 8377323f74c368c') unhappy('김치') # Kimchi unhappy('') - unhappy('') + unhappy(b'') unhappy(None) diff --git a/tests/test_web/test_utils.py b/tests/test_web/test_utils.py index ea1eff80..f14f3642 100644 --- a/tests/test_web/test_utils.py +++ b/tests/test_web/test_utils.py @@ -15,12 +15,12 @@ class IsValidIdTest(unittest.TestCase): self.assertTrue(utils.is_valid_id('deadbeefbeefcacedeadcace')) self.assertTrue(utils.is_valid_id('deadbeefbeefcacedeadcace')) - # 12-byte arbitrary ASCII strings - self.assertTrue(utils.is_valid_id('DeadBeefCake')) - self.assertTrue(utils.is_valid_id('DeadBeefCake')) + # 12-byte arbitrary ASCII bytes + self.assertTrue(utils.is_valid_id(b'DeadBeefCake')) + self.assertTrue(utils.is_valid_id(b'DeadBeefCake')) - # 12-byte str object - self.assertTrue(utils.is_valid_id('beef€67890')) + # 12-byte object + self.assertTrue(utils.is_valid_id('beef€67890'.encode())) def test_bad_length(self): self.assertFalse(utils.is_valid_id(23 * 'a')) From 3fe9472d27b1475a9030441c7d229eab012f4c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 14:16:29 +0100 Subject: [PATCH 13/31] Python 3.6 compatibility: random bits & bcrypt Switched from Sybren's RSA library to the new stdlib module 'secrets' to generate secret tokens. This also means that the rsa library was demoted to secondary requirement. --- pillar/api/local_auth.py | 8 ++++++-- pillar/api/nodes/__init__.py | 15 ++++++++++----- requirements.txt | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pillar/api/local_auth.py b/pillar/api/local_auth.py index 091c521b..19da1f84 100644 --- a/pillar/api/local_auth.py +++ b/pillar/api/local_auth.py @@ -5,7 +5,6 @@ import typing import bcrypt import datetime -import rsa.randnum from bson import tz_util from flask import abort, Blueprint, current_app, jsonify, request from pillar.api.utils.authentication import create_new_user_document @@ -77,7 +76,12 @@ def generate_and_store_token(user_id, days=15, prefix=b''): :return: the token document. """ - random_bits = rsa.randnum.read_random_bits(256) + if not isinstance(prefix, bytes): + raise TypeError('prefix must be bytes, not %s' % type(prefix)) + + import secrets + + random_bits = secrets.token_bytes(32) # Use 'xy' as altargs to prevent + and / characters from appearing. # We never have to b64decode the string anyway. diff --git a/pillar/api/nodes/__init__.py b/pillar/api/nodes/__init__.py index fab323fa..f2e65db6 100644 --- a/pillar/api/nodes/__init__.py +++ b/pillar/api/nodes/__init__.py @@ -4,7 +4,6 @@ import logging import urllib.parse import pymongo.errors -import rsa.randnum import werkzeug.exceptions as wz_exceptions from bson import ObjectId from flask import current_app, g, Blueprint, request @@ -150,13 +149,19 @@ def make_world_gettable(node): node_id) -def create_short_code(node): +def create_short_code(node) -> str: """Generates a new 'short code' for the node.""" + import secrets + length = current_app.config['SHORT_CODE_LENGTH'] - bits = rsa.randnum.read_random_bits(32) - short_code = base64.b64encode(bits, altchars='xy').rstrip('=') - short_code = short_code[:length] + + # Base64 encoding will expand it a bit, so we'll cut that off later. + # It's a good idea to start with enough bytes, though. + bits = secrets.token_bytes(length) + + short_code = base64.b64encode(bits, altchars=b'xy').rstrip(b'=') + short_code = short_code[:length].decode('ascii') return short_code diff --git a/requirements.txt b/requirements.txt index fea38733..84adc51a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ attrs==16.2.0 algoliasearch==1.8.0 -bcrypt==2.0.0 +bcrypt==3.1.3 blinker==1.4 bugsnag==2.3.1 bleach==1.4.3 @@ -27,7 +27,6 @@ pycrypto==2.6.1 pyOpenSSL==0.15.1 python-dateutil==2.5.3 requests==2.9.1 -rsa==3.4.2 simplejson==3.8.2 WebOb==1.5.0 wheel==0.29.0 @@ -52,5 +51,6 @@ pyasn1-modules==0.0.8 pymongo==3.2.2 pyparsing==2.1.10 requests-oauthlib==0.7.0 +rsa==3.4.2 six==1.10.0 WTForms==2.1 From a0a8257df0675c5d3820e50dd193a2c80e34a265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 14:42:13 +0100 Subject: [PATCH 14/31] Python 3.6: Fixed issue with gravatar function Hashing of string object doesn't work. Also added a deprecation warning that pillar.api.utils.gravatar should be used; pillar.web.utils.gravatar is just a copy. --- pillar/api/utils/__init__.py | 4 ++-- pillar/web/utils/__init__.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index 77fdc19c..81411445 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -141,10 +141,10 @@ def str2id(document_id): raise wz_exceptions.BadRequest('Invalid object ID %r' % document_id) -def gravatar(email, size=64): +def gravatar(email: str, size=64): parameters = {'s': str(size), 'd': 'mm'} return "https://www.gravatar.com/avatar/" + \ - hashlib.md5(str(email)).hexdigest() + \ + hashlib.md5(email.encode()).hexdigest() + \ "?" + urllib.parse.urlencode(parameters) diff --git a/pillar/web/utils/__init__.py b/pillar/web/utils/__init__.py index 67da2fb9..e40aa5f5 100644 --- a/pillar/web/utils/__init__.py +++ b/pillar/web/utils/__init__.py @@ -49,11 +49,13 @@ def attach_project_pictures(project, api): project.picture_header = get_file(project.picture_header, api=api) -def gravatar(email, size=64): - parameters = {'s': str(size), 'd': 'mm'} - return "https://www.gravatar.com/avatar/" + \ - hashlib.md5(str(email)).hexdigest() + \ - "?" + urllib.parse.urlencode(parameters) +def gravatar(email: str, size=64): + import warnings + warnings.warn("the pillar.web.gravatar function is deprecated; use hashlib instead", + DeprecationWarning, 2) + + from pillar.api.utils import gravatar as api_gravatar + return api_gravatar(email, size) def datetime_now(): From 662f1276d2efc5ee0b23a450adc9701661b32769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 3 Mar 2017 14:53:44 +0100 Subject: [PATCH 15/31] Upgraded development requirements to speed up code coverage recording Removed requests from requirements.txt file, because it's already a req of pillar-python-sdk. --- requirements-dev.txt | 8 ++++---- requirements.txt | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 32a73467..5e5b94ac 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,13 +3,13 @@ -e . # Development requirements -pytest==3.0.1 +pytest==3.0.6 responses==0.5.1 -pytest-cov==2.3.1 +pytest-cov==2.4.0 mock==2.0.0 # Secondary development requirements cookies==2.2.1 -coverage==4.0.3 -pbr==1.9.1 +coverage==4.3.4 +pbr==2.0.0 py==1.4.31 diff --git a/requirements.txt b/requirements.txt index 84adc51a..eb1b7002 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,6 @@ Pillow==2.8.1 pycrypto==2.6.1 pyOpenSSL==0.15.1 python-dateutil==2.5.3 -requests==2.9.1 simplejson==3.8.2 WebOb==1.5.0 wheel==0.29.0 From 47ca614ea38d1e2338c304d525707cfbbc60b83f Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Tue, 7 Mar 2017 12:49:17 +0100 Subject: [PATCH 16/31] Remove Linux venv specific dependencies Was giving install error on macOS. --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index eb1b7002..1bd4080f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,6 @@ zencoder==0.6.5 Flask-PyMongo==0.4.1 Jinja2==2.8 Werkzeug==0.11.10 -appdirs==1.4.2 CommonMark==0.7.2 future==0.15.2 html5lib==0.9999999 @@ -43,12 +42,10 @@ googleapis-common-protos==1.1.0 itsdangerous==0.24 oauth2client==2.0.2 oauthlib==2.0.1 -packaging==16.8 protobuf==3.0.0b2.post2 protorpc==0.11.1 pyasn1-modules==0.0.8 pymongo==3.2.2 -pyparsing==2.1.10 requests-oauthlib==0.7.0 rsa==3.4.2 six==1.10.0 From 4e36ea5aae7fc00b068d63a1a1fcd792c64ce78f Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Tue, 7 Mar 2017 12:50:03 +0100 Subject: [PATCH 17/31] Remove redundant requirement It is already defined in requirements.txt in pillar-python-sdk. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1bd4080f..2bc9791c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,6 @@ MarkupSafe==0.23 ndg-httpsclient==0.4.0 Pillow==2.8.1 pycrypto==2.6.1 -pyOpenSSL==0.15.1 python-dateutil==2.5.3 simplejson==3.8.2 WebOb==1.5.0 From 7967b80ab39c5f7d8305a47c3402c033e227fb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 7 Mar 2017 14:25:34 +0100 Subject: [PATCH 18/31] Auto-install -e pillar It uses ../pillar instead of . so that it is a valid path from blender-cloud as well. --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5e5b94ac..0bc03fb2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ -r requirements.txt -e ../pillar-python-sdk --e . +-e ../pillar # also works from parent project, like blender-cloud # Development requirements pytest==3.0.6 From a7cd515fdb8fa353df223e41913eb666a979560e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 8 Mar 2017 17:06:55 +0100 Subject: [PATCH 19/31] Fixed version conflict with pillarsdk dev requirements --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0bc03fb2..016bd4c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,4 +12,4 @@ mock==2.0.0 cookies==2.2.1 coverage==4.3.4 pbr==2.0.0 -py==1.4.31 +py==1.4.32 From db98c681a28733ee3387b74a75a55408f2b16d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 8 Mar 2017 17:16:44 +0100 Subject: [PATCH 20/31] Added missing redis requirement --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 2bc9791c..9a9b0aad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ ndg-httpsclient==0.4.0 Pillow==2.8.1 pycrypto==2.6.1 python-dateutil==2.5.3 +redis==2.10.5 simplejson==3.8.2 WebOb==1.5.0 wheel==0.29.0 From 9abdd1ee9032dd2dbf544dbd0382330ee20039ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 7 Mar 2017 14:36:35 +0100 Subject: [PATCH 21/31] Synced dev requirements with Pillar Python SDK --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 016bd4c8..f628b3fa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -r requirements.txt --e ../pillar-python-sdk +-r ../pillar-python-sdk/requirements-dev.txt -e ../pillar # also works from parent project, like blender-cloud # Development requirements From 1eb1cd7b64cb2ec112877f161fe1d77eb6a0cb4a Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Sun, 12 Mar 2017 18:46:08 +0100 Subject: [PATCH 22/31] Fix for crash on extension blueprints loading It the extension was registered with url_prefix=None, we set url_prefix to empty string so it can be added to blueprint.url_prefix. --- pillar/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pillar/__init__.py b/pillar/__init__.py index a1b2c2e6..3f463b01 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -206,6 +206,9 @@ class PillarServer(Eve): # Load extension blueprint(s) for blueprint in pillar_extension.blueprints(): if blueprint.url_prefix: + if not url_prefix: + # If we registered the extension with url_prefix=None + url_prefix = '' blueprint_prefix = url_prefix + blueprint.url_prefix else: blueprint_prefix = url_prefix From bfb5f4f44e3905f1e2459c9521b9a354a0354f11 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Mon, 13 Mar 2017 17:11:06 +0100 Subject: [PATCH 23/31] Video Player: Vertical volume slider and loop by default --- src/templates/nodes/custom/asset/video/view_embed.jade | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/templates/nodes/custom/asset/video/view_embed.jade b/src/templates/nodes/custom/asset/video/view_embed.jade index 16f9d395..add8cd4c 100644 --- a/src/templates/nodes/custom/asset/video/view_embed.jade +++ b/src/templates/nodes/custom/asset/video/view_embed.jade @@ -133,7 +133,14 @@ script(type="text/javascript"). {% if node.video_sources %} videojs(document.getElementById('videoplayer'), { - fluid: true + fluid: true, + controlBar: { + volumeMenuButton: { + inline: false, + vertical: true + } + }, + loop: true, }, function(){ this.ga({ From c52bfd2236d0c3a3743dbb234ee476c24dd55f32 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Fri, 17 Mar 2017 16:53:05 +0100 Subject: [PATCH 24/31] Tweaks to group listing Non-square thumbnails, always display icon type --- src/styles/_config.sass | 3 +- src/styles/_project.sass | 77 ++++++++----------- .../nodes/custom/group/view_embed.jade | 61 +++------------ .../nodes/custom/group_hdri/view_embed.jade | 3 - 4 files changed, 45 insertions(+), 99 deletions(-) diff --git a/src/styles/_config.sass b/src/styles/_config.sass index 8caeb448..ab14b408 100644 --- a/src/styles/_config.sass +++ b/src/styles/_config.sass @@ -112,8 +112,9 @@ $node-type-comment: #66ccbd $node-type-group: #c3a280 $node-type-post: #647bce -$list-node-children-item-width: 160px +$list-node-children-item-width: 200px $list-node-children-item-width_list: 48px +$list-node-children-item-width_square: 160px $z-index-base: 13 .container diff --git a/src/styles/_project.sass b/src/styles/_project.sass index b2ef0dec..665f8d60 100644 --- a/src/styles/_project.sass +++ b/src/styles/_project.sass @@ -946,7 +946,6 @@ section.node-row .node-title color: $color-text-dark-primary - text-transform: capitalize font: family: $font-body weight: 500 @@ -1743,14 +1742,15 @@ section.node-children .list-node-children-item-thumbnail width: 100% + height: $list-node-children-item-width +media-sm img width: 100% .list-node-children-item-thumbnail-icon i - position: relative - bottom: 10px + bottom: 30px + .list-node-children-item-name bottom: 26px width: 100% @@ -1818,26 +1818,17 @@ section.node-children .list-node-children-item float: left - margin: 10px 15px 10px 0 + margin: 10px 10px 10px 0 border-radius: 3px - - color: $color-background-nav - background-color: darken(white, 6%) - - box-shadow: 2px 2px 0 rgba(darken($color-background, 60%), .1) + color: $color-text-dark + box-shadow: 2px 2px 0 rgba(black, .1) width: $list-node-children-item-width overflow: hidden - text-overflow: ellipsis &:hover color: darken($color-primary, 20%) - - & .list-node-children-item-name i - color: darken($color-primary, 20%) - - & .list-node-children-item-thumbnail - & .list-node-children-item-thumbnail-icon - transform: translate(-50%, -50%) scale(1.1) + opacity: .9 + text-decoration: none &:active opacity: .8 @@ -1875,9 +1866,6 @@ section.node-children .list-node-children-item-thumbnail cursor: pointer - .list-node-children-item-thumbnail-icon - transform: translate(-50%, -50%) scale(1) - &:active background-color: rgba($color-background, .5) &:focus @@ -1960,20 +1948,30 @@ section.node-children justify-content: center width: $list-node-children-item-width - height: $list-node-children-item-width - background-color: darken($color-background, 5%) + height: $list-node-children-item-width / 1.69 + background-color: darken($color-background, 30%) position: relative overflow: hidden + img + height: 100% + +position-center-translate + & .list-node-children-item-thumbnail-icon position: absolute - top: 50% - left: 50% - transform: translate(-50%, -50%) - font-size: 4em + top: 0 + left: 0 + right: 0 + bottom: 0 + font-size: 1.5em color: white - text-shadow: 1px 1px 1px rgba(black, .2), 0 0 50px rgba(black, .3) - transition: transform .1s ease-in-out + text-shadow: 1px 1px 0 rgba(black, .2) + background-image: linear-gradient(10deg, darken($color-background, 55%) 0%, transparent 40%) + + i + position: absolute + bottom: 5px + left: 5px & .list-node-children-item-status color: $color-text-light-primary @@ -1997,33 +1995,26 @@ section.node-children display: flex align-items: center - background-color: darken(white, 6%) + // background-color: darken(white, 6%) + color: $color-text-light + text-shadow: 1px 1px 0 rgba(black, .2) + background-color: darken($color-background, 55%) - padding: - top: 5px - bottom: 5px - font-size: .9em + padding: 5px 10px + font-size: .85em width: $list-node-children-item-width max-width: $list-node-children-item-width - i - color: $color-text-dark-secondary - padding: - left: 7px - right: 3px - font-size: .9em - span - padding: - left: 0 - right: 3px +text-overflow-ellipsis &.texture, &.hdri margin: 5px 10px 5px 0 position: relative + width: $list-node-children-item-width height: $list-node-children-item-width .list-node-children-item-thumbnail + height: $list-node-children-item-width background-color: $color-background-nav &:hover diff --git a/src/templates/nodes/custom/group/view_embed.jade b/src/templates/nodes/custom/group/view_embed.jade index 846d95d2..dcb4a170 100644 --- a/src/templates/nodes/custom/group/view_embed.jade +++ b/src/templates/nodes/custom/group/view_embed.jade @@ -12,8 +12,8 @@ section.node-details-container.group .node-details-meta.preview ul.node-details-meta-list - li.node-details-meta-list-item.date(title="Created {{ node._created | pretty_date }}") - | {{ node._updated | pretty_date }} + li.node-details-meta-list-item.date(title="Updated {{ node._updated | pretty_date }}") + | {{ node._created | pretty_date }} li.node-details-meta-list-item.author | {{ node.user.full_name }} @@ -53,32 +53,6 @@ src="{{ child.picture.thumbnail('t', api=api)}} ") | {% endif %} - .list-node-children-item-thumbnail-icon - | {# If there's a type available, otherwise show a folder icon #} - | {% if child.properties.content_type %} - - | {# Show an icon if there's no thumbnail #} - | {% if not child.picture %} - | {% if child.properties.content_type == 'image' %} - i.dark.pi-image - | {% elif child.properties.content_type == 'video' %} - i.dark.pi-film-thick - | {% elif child.properties.content_type == 'file' %} - i.dark.pi-document - | {% endif %} - - | {% else %} - | {% if child.properties.content_type == 'video' %} - i.pi-play - | {% endif %} - | {% endif %} - - | {% else %} - | {% if not child.picture %} - i.dark.pi-folder - | {% endif %} - | {% endif %} - | {% if child.permissions.world %} .list-node-children-item-ribbon span free @@ -103,13 +77,7 @@ | {% endif %} | {% endif %} - | {% if child._updated %} - span(title="Updated on {{ child._created }}") {{ child._updated | pretty_date }} - span.updated(title="Created on {{ child._updated }}") * - | {% else %} - span(title="Created on {{ child._created }}") {{ child._created | pretty_date }} - | {% endif %} - + span(title="Created on {{ child._created }}") {{ child._created | pretty_date }} | {# Browse type: Icon #} a(href="{{ url_for_node(node=child) }}", @@ -120,16 +88,16 @@ | {% if child.picture %} img( - src="{{ child.picture.thumbnail('b', api=api)}} ") + src="{{ child.picture.thumbnail('m', api=api)}} ") | {% endif %} .list-node-children-item-thumbnail-icon - | {% if child.properties.content_type %} - - | {% if child.properties.content_type == 'video' %} + | {% if child.properties.content_type and child.properties.content_type == 'video' %} i.pi-play - | {% endif %} - + | {% elif child.properties.content_type and child.properties.content_type == 'image' %} + i.pi-image + | {% elif child.properties.content_type and child.properties.content_type == 'file' %} + i.pi-file-archive | {% else %} i.pi-folder | {% endif %} @@ -144,17 +112,6 @@ | {% endif %} .list-node-children-item-name - - | {% if child.properties.content_type == 'video' %} - i.pi-film-thick - | {% elif child.properties.content_type == 'image' %} - i.pi-image - | {% elif child.properties.content_type == 'file' %} - i.pi-document - | {% else %} - i.pi-folder - | {% endif %} - span {{ child.name }} | {% endfor %} diff --git a/src/templates/nodes/custom/group_hdri/view_embed.jade b/src/templates/nodes/custom/group_hdri/view_embed.jade index 1ef9b951..bbf6bcac 100644 --- a/src/templates/nodes/custom/group_hdri/view_embed.jade +++ b/src/templates/nodes/custom/group_hdri/view_embed.jade @@ -77,11 +77,8 @@ | {% endif %} | {% endif %} - | {% if child.node_type == 'group_hdri' %} .list-node-children-item-name - i.pi-folder-texture span {{ child.name }} - | {% endif %} | {% endif %} | {% endfor %} From ba5923044a1e22b846164e864650e9522d02e686 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Fri, 17 Mar 2017 18:03:06 +0100 Subject: [PATCH 25/31] Minor tweaks to homepage listing Thumbnails are now slightly larger (22px wider), and did some rearrangement --- src/styles/_homepage.sass | 107 +++++++++++++++++++++--------------- src/templates/homepage.jade | 55 ++++++++++-------- 2 files changed, 97 insertions(+), 65 deletions(-) diff --git a/src/styles/_homepage.sass b/src/styles/_homepage.sass index ddada908..24eef1e9 100644 --- a/src/styles/_homepage.sass +++ b/src/styles/_homepage.sass @@ -162,13 +162,14 @@ padding: 15px 0 list-style: none + $activity-stream-thumbnail-size: 92px li.activity-stream__list-item position: relative display: flex - margin: 0 10px 10px + margin: 0 10px 5px background-color: white border-radius: 3px - box-shadow: 0 0 0 1px rgba(black, .05), 0 2px 5px rgba(black, .1) + box-shadow: 0 0 0 1px rgba(black, .05), 0 2px 2px rgba(black, .1) &.active .activity-stream__list-details .title color: $color-primary @@ -206,37 +207,33 @@ .activity-stream__list-thumbnail background: transparent border-color: $node-type-comment - width: 35px - height: auto - min-width: 35px - min-height: auto color: $node-type-comment - font-size: 1.25em + font-size: 1.2em + box-shadow: none i - position: absolute + +position-center-translate left: 22px - top: 17px + top: 19px .activity-stream__list-details + padding: 0 .title color: $color-text-dark - padding: 5px 10px 2px 10px + padding: 7px 10px 2px 10px font-size: 1em margin: 0 ul.meta - padding: 0 10px 5px 10px - margin: 0 + padding: 0 10px 7px 10px - li.where-parent:before - content: '\e83a' - font-family: 'pillar-font' + li + &.where-parent:before + content: '\e83a' + font-family: 'pillar-font' - li.when - float: right - &:before - content: '' + &.what:before + display: none &.post .activity-stream__list-thumbnail @@ -262,36 +259,60 @@ text-decoration: underline cursor: pointer - $activity-stream-thumbnail-size: 75px + &.with-picture + min-height: $activity-stream-thumbnail-size + + .activity-stream__list-thumbnail + background-color: black + width: $activity-stream-thumbnail-size * 1.69 + min-width: $activity-stream-thumbnail-size * 1.69 + + .activity-stream__list-thumbnail-icon + position: absolute + top: 0 + left: 0 + right: 0 + bottom: 0 + font-size: 1.3em + text-shadow: 1px 1px 0 rgba(black, .2) + background-image: linear-gradient(10deg, rgba(black, .5) 0%, transparent 40%) + + i + position: absolute + bottom: -8px + left: 20px + top: initial + right: initial + color: white + .activity-stream__list-thumbnail position: relative display: flex justify-content: center align-items: center overflow: hidden - width: $activity-stream-thumbnail-size * 1.69 - min-width: $activity-stream-thumbnail-size * 1.69 - min-height: $activity-stream-thumbnail-size - font-size: 1.8em - color: rgba(white, .5) - background-color: $color-background-light - border-top-left-radius: 3px - border-bottom-left-radius: 3px + width: 35px + height: auto + min-width: 35px + min-height: auto +media-xs display: none - &.image - background-color: $node-type-asset_image - &.file - background-color: $node-type-asset_file - &.video - background-color: $node-type-asset_video - i.pi-film-thick - font-size: .8em + + &.image i + color: $node-type-asset_image + &.file i + color: $node-type-asset_file + &.video i + color: $node-type-asset_video i +position-center-translate + left: 23px + top: 21px + font-size: 1.1em + img max-height: $activity-stream-thumbnail-size +position-center-translate @@ -299,12 +320,13 @@ .activity-stream__list-details display: flex flex-direction: column - justify-content: center + justify-content: space-around flex: 1 overflow: hidden position: relative max-width: 100% margin-right: auto + padding: 10px 0 +media-xs margin-left: 0 @@ -322,7 +344,7 @@ .title display: inline-block - padding: 5px 15px + padding: 0 10px color: $color-text-dark font-size: 1.1em @@ -334,22 +356,21 @@ ul.meta +list-meta - padding: 0 10px 7px 15px + padding: 5px 10px 0 10px font-size: .85em color: $color-text-dark-secondary display: flex white-space: nowrap + &.extra + margin-top: auto + li padding-left: 10px &:before left: -5px &.where-project +text-overflow-ellipsis - &.when - margin-left: auto - &:before - display: none ul#activity-stream__filters margin: 0 diff --git a/src/templates/homepage.jade b/src/templates/homepage.jade index aa2ebebd..4025127e 100644 --- a/src/templates/homepage.jade +++ b/src/templates/homepage.jade @@ -49,31 +49,30 @@ meta(name="twitter:image", content="{% if main_project.picture_header %}{{ main_ ul#activity-stream__list | {% for n in activity_stream %} li.activity-stream__list-item( - class="{{ n.node_type }} {{ n.properties.content_type }}", + class="{{ n.node_type }} {{ n.properties.content_type }} {% if n.picture %}with-picture{% endif %}", data-url="{{ url_for_node(node=n) }}") a.activity-stream__list-thumbnail( - class="{{ n.properties.content_type }} {% if n.picture %}with-picture{% endif %}", + class="{{ n.properties.content_type }}", href="{{ url_for_node(node=n) }}") | {% if n.picture %} img(src="{{ n.picture.thumbnail('m', api=api) }}") - | {% else %} + | {% endif %} + + .activity-stream__list-thumbnail-icon + | {% if n.node_type == 'asset' %} + | {% if n.properties.content_type == 'video' %} + i.pi-play + | {% elif n.properties.content_type == 'image' %} + i.pi-picture + | {% elif n.properties.content_type == 'file' %} + i.pi-file-archive + | {% else %} + i.pi-folder + | {% endif %} + | {% elif n.node_type == 'comment' %} + i.pi-comment + | {% endif %} - | {% if n.node_type == 'asset' %} - | {% if n.properties.content_type == 'video' %} - i.pi-film-thick - | {% elif n.properties.content_type == 'image' %} - i.pi-picture - | {% elif n.properties.content_type == 'file' %} - i.pi-file-archive - | {% else %} - i.pi-folder - | {% endif %} - | {% elif n.node_type == 'post' %} - i.pi-newspaper - | {% elif n.node_type == 'comment' %} - i.pi-comment - | {% endif %} - | {% endif %} .activity-stream__list-details a.title(href="{{ url_for_node(node=n) }}") @@ -87,19 +86,31 @@ meta(name="twitter:image", content="{% if main_project.picture_header %}{{ main_ span free | {% endif %} ul.meta - li.what - | {% if n.node_type == 'asset' %}{{ n.properties.content_type | undertitle }}{% else %}{{ n.node_type | undertitle }}{% endif %} - | {% if n.node_type != 'post' %} + | {% if n.node_type == 'comment' or not n.picture %} + li.when + a(href="{{ url_for_node(node=n) }}", title="{{ n._created }}") {{ n._created | pretty_date_time }} li.who {{ n.user.full_name }} | {% endif %} + | {% if n.attached_to %} li.where-parent a(href="{{ url_for_node(node_id=n.attached_to._id) }}") {{ n.attached_to.name }} | {% endif %} li.where-project a.project(href="{{ url_for('projects.view', project_url=n.project.url) }}") {{ n.project.name }} + li.what + | {% if n.node_type == 'asset' %} + | {{ n.properties.content_type | undertitle }} + | {% elif n.node_type != 'comment' %} + | {{ n.node_type | undertitle }} + | {% endif %} + + | {% if n.picture %} + ul.meta.extra li.when a(href="{{ url_for_node(node=n) }}", title="{{ n._created }}") {{ n._created | pretty_date_time }} + li.who {{ n.user.full_name }} + | {% endif %} | {% endfor %} li.activity-stream__list-item.empty#activity-stream__empty From 508a28aeaee74756ba4895fd414bb79cbb25f021 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Fri, 17 Mar 2017 18:05:12 +0100 Subject: [PATCH 26/31] Update CSS caches --- src/templates/layout.jade | 14 +++++++------- src/templates/nodes/custom/blog/index.jade | 8 ++++---- src/templates/nodes/custom/post/view.jade | 2 +- src/templates/projects/view.jade | 12 ++++++------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/templates/layout.jade b/src/templates/layout.jade index af2b1321..b4c47865 100644 --- a/src/templates/layout.jade +++ b/src/templates/layout.jade @@ -38,8 +38,8 @@ html(lang="en") loadCSS( "//fonts.googleapis.com/css?family=Roboto:300,400" ); - script(src="{{ url_for('static_pillar', filename='assets/js/markdown.min.js', v=6220171) }}") - script(src="{{ url_for('static_pillar', filename='assets/js/tutti.min.js', v=6220171) }}") + script(src="{{ url_for('static_pillar', filename='assets/js/markdown.min.js', v=17320171) }}") + script(src="{{ url_for('static_pillar', filename='assets/js/tutti.min.js', v=17320171) }}") link(href="{{ url_for('static', filename='assets/img/favicon.png') }}", rel="shortcut icon") link(href="{{ url_for('static', filename='assets/img/apple-touch-icon-precomposed.png') }}", rel="icon apple-touch-icon-precomposed", sizes="192x192") @@ -49,12 +49,12 @@ html(lang="en") | {% block head %}{% endblock %} | {% block css %} - link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=6220171) }}", rel="stylesheet") - link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=6220171) }}", rel="stylesheet") + link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=17320171) }}", rel="stylesheet") + link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=17320171) }}", rel="stylesheet") | {% if title == 'blog' %} - link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=6220171) }}", rel="stylesheet") + link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=17320171) }}", rel="stylesheet") | {% else %} - link(href="{{ url_for('static_pillar', filename='assets/css/main.css', v=6220171) }}", rel="stylesheet") + link(href="{{ url_for('static_pillar', filename='assets/css/main.css', v=17320171) }}", rel="stylesheet") | {% endif %} | {% endblock %} @@ -408,7 +408,7 @@ html(lang="en") noscript link(href='//fonts.googleapis.com/css?family=Roboto:300,400', rel='stylesheet', type='text/css') - script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.bootstrap-3.3.7.min.js', v=6220171) }}") + script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.bootstrap-3.3.7.min.js', v=17320171) }}") script. $(document).ready(function() { diff --git a/src/templates/nodes/custom/blog/index.jade b/src/templates/nodes/custom/blog/index.jade index b7fa7335..382e870b 100644 --- a/src/templates/nodes/custom/blog/index.jade +++ b/src/templates/nodes/custom/blog/index.jade @@ -3,10 +3,10 @@ | {% block page_title %}Blog{% endblock%} | {% block css %} -link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=6220171) }}", rel="stylesheet") -link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=6220171) }}", rel="stylesheet") -link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v=6220171) }}", rel="stylesheet") -link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=6220171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=17320171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=17320171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v=17320171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=17320171) }}", rel="stylesheet") | {% endblock %} | {% block project_context %} diff --git a/src/templates/nodes/custom/post/view.jade b/src/templates/nodes/custom/post/view.jade index a4ef18c2..aa3a9ec5 100644 --- a/src/templates/nodes/custom/post/view.jade +++ b/src/templates/nodes/custom/post/view.jade @@ -5,7 +5,7 @@ | {% block css %} | {{ super() }} -link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=6220171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=17320171) }}", rel="stylesheet") | {% endblock %} | {% block project_context %} diff --git a/src/templates/projects/view.jade b/src/templates/projects/view.jade index 00ad474f..fccb5740 100644 --- a/src/templates/projects/view.jade +++ b/src/templates/projects/view.jade @@ -71,14 +71,14 @@ link(href="//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.1/themes/default/style.mi link(rel="amphtml", href="{{ url_for('nodes.view', node_id=node._id, _external=True, format='amp') }}") | {% endif %} -script(src="{{ url_for('static_pillar', filename='assets/js/vendor/videojs-5.8.8.min.js', v=6220171) }}") -script(src="{{ url_for('static_pillar', filename='assets/js/vendor/videojs-ga-0.4.2.min.js', v=6220171) }}") +script(src="{{ url_for('static_pillar', filename='assets/js/vendor/videojs-5.8.8.min.js', v=17320171) }}") +script(src="{{ url_for('static_pillar', filename='assets/js/vendor/videojs-ga-0.4.2.min.js', v=17320171) }}") | {% endblock %} | {% block css %} -link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=6220171) }}", rel="stylesheet") -link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=6220171) }}", rel="stylesheet") -link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v=6220171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=17320171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=17320171) }}", rel="stylesheet") +link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v=17320171) }}", rel="stylesheet") | {% endblock %} | {% block body %} @@ -270,7 +270,7 @@ link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v | {% if project.has_method('PUT') %} | {# JS containing the Edit, Add, Featured, and Move functions #} -script(type="text/javascript", src="{{ url_for('static_pillar', filename='assets/js/project-edit.min.js', v=6220171) }}") +script(type="text/javascript", src="{{ url_for('static_pillar', filename='assets/js/project-edit.min.js', v=17320171) }}") | {% endif %} script. From b70bc07a75b56d60bfd99e260d5125e8ab97b601 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Mon, 20 Mar 2017 15:54:36 +0100 Subject: [PATCH 27/31] Slightly bigger thumbnail size for posts in homepage --- src/styles/_homepage.sass | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/styles/_homepage.sass b/src/styles/_homepage.sass index 24eef1e9..fcd48a0f 100644 --- a/src/styles/_homepage.sass +++ b/src/styles/_homepage.sass @@ -162,7 +162,7 @@ padding: 15px 0 list-style: none - $activity-stream-thumbnail-size: 92px + $activity-stream-thumbnail-size: 110px li.activity-stream__list-item position: relative display: flex @@ -170,6 +170,7 @@ background-color: white border-radius: 3px box-shadow: 0 0 0 1px rgba(black, .05), 0 2px 2px rgba(black, .1) + overflow: hidden &.active .activity-stream__list-details .title color: $color-primary From f19be0ae17af1828242fde2ecb8dc06c930b97e9 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Mon, 20 Mar 2017 16:00:37 +0100 Subject: [PATCH 28/31] Larger image for collections header --- src/templates/projects/index_collection.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/projects/index_collection.jade b/src/templates/projects/index_collection.jade index 70ef14f0..cca0a0eb 100644 --- a/src/templates/projects/index_collection.jade +++ b/src/templates/projects/index_collection.jade @@ -52,7 +52,7 @@ meta(name="twitter:image", content="{% if title == 'training' %}{{ url_for('stat | {% if project.picture_header %} a.item-header( href="{{ url_for('projects.view', project_url=project.url) }}") - img(src="{{ project.picture_header.thumbnail('m', api=api) }}") + img(src="{{ project.picture_header.thumbnail('l', api=api) }}") | {% endif %} .item-info From fde50f652599f9c950c7cadbc3d5e9f805a401e4 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Mon, 20 Mar 2017 18:07:13 +0100 Subject: [PATCH 29/31] Groups view: Background gradient gray doesn't look good --- src/styles/_project.sass | 20 +++++++++++++------ .../nodes/custom/group/view_embed.jade | 5 ++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/styles/_project.sass b/src/styles/_project.sass index 665f8d60..b167c7b2 100644 --- a/src/styles/_project.sass +++ b/src/styles/_project.sass @@ -1836,6 +1836,16 @@ section.node-children & .list-node-children-item-name i color: $color-primary + &.has-picture + .list-node-children-item-thumbnail + background-color: black + + .cloud-logo + font-size: 4em + color: darken($color-background, 60%) + +position-center-translate + left: $list-node-children-item-width / 2 - 10 + /* Browse group as list */ &.browse-list display: none @@ -1949,7 +1959,7 @@ section.node-children width: $list-node-children-item-width height: $list-node-children-item-width / 1.69 - background-color: darken($color-background, 30%) + background-color: darken($color-background, 55%) position: relative overflow: hidden @@ -1963,10 +1973,10 @@ section.node-children left: 0 right: 0 bottom: 0 - font-size: 1.5em + font-size: 1.3em color: white text-shadow: 1px 1px 0 rgba(black, .2) - background-image: linear-gradient(10deg, darken($color-background, 55%) 0%, transparent 40%) + background-image: linear-gradient(10deg, rgba(black, .2) 0%, transparent 40%) i position: absolute @@ -1994,11 +2004,9 @@ section.node-children display: flex align-items: center - - // background-color: darken(white, 6%) color: $color-text-light text-shadow: 1px 1px 0 rgba(black, .2) - background-color: darken($color-background, 55%) + background-color: darken($color-background, 60%) padding: 5px 10px font-size: .85em diff --git a/src/templates/nodes/custom/group/view_embed.jade b/src/templates/nodes/custom/group/view_embed.jade index dcb4a170..3a432181 100644 --- a/src/templates/nodes/custom/group/view_embed.jade +++ b/src/templates/nodes/custom/group/view_embed.jade @@ -83,12 +83,15 @@ a(href="{{ url_for_node(node=child) }}", data-node_id="{{ child._id }}", title="{{ child.name }}", - class="item_icon list-node-children-item browse-icon") + class="item_icon list-node-children-item browse-icon {% if child.picture %}has-picture{% endif %}") .list-node-children-item-thumbnail | {% if child.picture %} img( src="{{ child.picture.thumbnail('m', api=api)}} ") + | {% else %} + .cloud-logo + i.pi-blender-cloud | {% endif %} .list-node-children-item-thumbnail-icon From e6fb64621f7d74d61b6ec8524c1ca606196ba805 Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Tue, 21 Mar 2017 15:45:36 +0100 Subject: [PATCH 30/31] Introducing GOOGLE_SITE_VERIFICATION Used for cross-verification on various Google sites (eg. YouTube) . By default it is not rendered in the pages. --- pillar/config.py | 2 ++ src/templates/layout.jade | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pillar/config.py b/pillar/config.py index 835a0fac..e640da78 100644 --- a/pillar/config.py +++ b/pillar/config.py @@ -53,6 +53,8 @@ BIN_RSYNC = '/usr/bin/rsync' GCLOUD_APP_CREDENTIALS = 'google_app.json' GCLOUD_PROJECT = '-SECRET-' +# Used for cross-verification on various Google sites (eg. YouTube) +GOOGLE_SITE_VERIFICATION = '' ADMIN_USER_GROUP = '5596e975ea893b269af85c0e' SUBSCRIBER_USER_GROUP = '5596e975ea893b269af85c0f' diff --git a/src/templates/layout.jade b/src/templates/layout.jade index b4c47865..4b107859 100644 --- a/src/templates/layout.jade +++ b/src/templates/layout.jade @@ -8,6 +8,10 @@ html(lang="en") meta(name="author", content="Blender Institute") meta(name="theme-color", content="#3e92aa") + | {% if config['GOOGLE_SITE_VERIFICATION'] %} + meta(name="google-site-verification" content="{{ config['GOOGLE_SITE_VERIFICATION'] }}") + | {% endif %} + meta(property="og:site_name", content="Blender Cloud") meta(property="og:locale", content="en_US") meta(name="twitter:card", content="summary_large_image") From a3513aa45cb54775b87b1eead440b4ab757d259d Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 22 Mar 2017 21:56:43 +0100 Subject: [PATCH 31/31] Update background for Agent 327 on join pages --- src/styles/_join.sass | 2 +- src/templates/join.jade | 2 +- src/templates/join_agent.jade | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/_join.sass b/src/styles/_join.sass index ccad96a2..9a598f9f 100644 --- a/src/styles/_join.sass +++ b/src/styles/_join.sass @@ -6,7 +6,7 @@ #page-header display: block - padding: 30px 0 15px 0 + padding: 30px 0 150px 0 font-size: .9em .container diff --git a/src/templates/join.jade b/src/templates/join.jade index 53a50f0d..f9f953d2 100644 --- a/src/templates/join.jade +++ b/src/templates/join.jade @@ -11,7 +11,7 @@ meta(property="og:image", content="{{ url_for('static', filename='assets/img/bac | {% block header_backdrop %} .navbar-backdrop.join( - style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/background_agent327_02.jpg')}})") + style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/background_andy_waking.jpg')}})") | {% endblock %} | {% block page_overlay %} diff --git a/src/templates/join_agent.jade b/src/templates/join_agent.jade index b17c9093..2c27a5d0 100644 --- a/src/templates/join_agent.jade +++ b/src/templates/join_agent.jade @@ -2,7 +2,7 @@ | {% block body %} .join-project( - style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/background_agent327_02.jpg')}})") + style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/background_agent327_03.jpg')}})") .join-project__title span.icon