This fixes all roles & group memberships at once, and fixes a bug where the script had to be re-run to apply multiple role changes on a single user.
1058 lines
35 KiB
Python
Executable File
1058 lines
35 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
from __future__ import print_function
|
|
from __future__ import division
|
|
|
|
import copy
|
|
import os
|
|
import logging
|
|
|
|
from bson.objectid import ObjectId, InvalidId
|
|
from eve.methods.put import put_internal
|
|
from eve.methods.post import post_internal
|
|
from flask.ext.script import Manager
|
|
|
|
# Use a sensible default when running manage.py commands.
|
|
if not os.environ.get('EVE_SETTINGS'):
|
|
settings_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
'settings.py')
|
|
os.environ['EVE_SETTINGS'] = settings_path
|
|
|
|
from application import app
|
|
from application.utils.gcs import GoogleCloudStorageBucket
|
|
from manage_extra.node_types.asset import node_type_asset
|
|
from manage_extra.node_types.blog import node_type_blog
|
|
from manage_extra.node_types.comment import node_type_comment
|
|
from manage_extra.node_types.group import node_type_group
|
|
from manage_extra.node_types.post import node_type_post
|
|
from manage_extra.node_types.project import node_type_project
|
|
from manage_extra.node_types.storage import node_type_storage
|
|
from manage_extra.node_types.texture import node_type_texture
|
|
from manage_extra.node_types.group_texture import node_type_group_texture
|
|
|
|
manager = Manager(app)
|
|
|
|
log = logging.getLogger('manage')
|
|
log.setLevel(logging.INFO)
|
|
|
|
MONGO_HOST = os.environ.get('MONGO_HOST', 'localhost')
|
|
|
|
|
|
@manager.command
|
|
def runserver(**options):
|
|
# Automatic creation of STORAGE_DIR path if it's missing
|
|
if not os.path.exists(app.config['STORAGE_DIR']):
|
|
os.makedirs(app.config['STORAGE_DIR'])
|
|
|
|
app.run(host=app.config['HOST'],
|
|
port=app.config['PORT'],
|
|
debug=app.config['DEBUG'],
|
|
**options)
|
|
|
|
|
|
@manager.command
|
|
def runserver_memlimit(limit_kb=1000000):
|
|
import resource
|
|
|
|
limit_b = int(limit_kb) * 1024
|
|
for rsrc in (resource.RLIMIT_AS, resource.RLIMIT_DATA, resource.RLIMIT_RSS):
|
|
resource.setrlimit(rsrc, (limit_b, limit_b))
|
|
|
|
runserver()
|
|
|
|
|
|
@manager.command
|
|
def runserver_profile(pfile='profile.stats'):
|
|
import cProfile
|
|
|
|
cProfile.run('runserver(use_reloader=False)', pfile)
|
|
|
|
|
|
def each_project_node_type(node_type_name=None):
|
|
"""Generator, yields (project, node_type) tuples for all projects and node types.
|
|
|
|
When a node type name is given, only yields those node types.
|
|
"""
|
|
|
|
projects_coll = app.data.driver.db['projects']
|
|
|
|
for project in projects_coll.find():
|
|
for node_type in project['node_types']:
|
|
if node_type_name is None or node_type['name'] == node_type_name:
|
|
yield project, node_type
|
|
|
|
|
|
def post_item(entry, data):
|
|
return post_internal(entry, data)
|
|
|
|
|
|
def put_item(collection, item):
|
|
item_id = item['_id']
|
|
internal_fields = ['_id', '_etag', '_updated', '_created']
|
|
for field in internal_fields:
|
|
item.pop(field, None)
|
|
# print item
|
|
# print type(item_id)
|
|
p = put_internal(collection, item, **{'_id': item_id})
|
|
if p[0]['_status'] == 'ERR':
|
|
print(p)
|
|
print(item)
|
|
|
|
|
|
@manager.command
|
|
def setup_db(admin_email):
|
|
"""Setup the database
|
|
- Create admin, subscriber and demo Group collection
|
|
- Create admin user (must use valid blender-id credentials)
|
|
- Create one project
|
|
"""
|
|
|
|
# Create default groups
|
|
groups_list = []
|
|
for group in ['admin', 'subscriber', 'demo']:
|
|
g = {'name': group}
|
|
g = post_internal('groups', g)
|
|
groups_list.append(g[0]['_id'])
|
|
print("Creating group {0}".format(group))
|
|
|
|
# Create admin user
|
|
user = {'username': admin_email,
|
|
'groups': groups_list,
|
|
'roles': ['admin', 'subscriber', 'demo'],
|
|
'settings': {'email_communications': 1},
|
|
'auth': [],
|
|
'full_name': admin_email,
|
|
'email': admin_email}
|
|
result, _, _, status = post_internal('users', user)
|
|
if status != 201:
|
|
raise SystemExit('Error creating user {}: {}'.format(admin_email, result))
|
|
user.update(result)
|
|
print("Created user {0}".format(user['_id']))
|
|
|
|
# Create a default project by faking a POST request.
|
|
with app.test_request_context(data={'project_name': u'Default Project'}):
|
|
from flask import g
|
|
from application.modules import projects
|
|
|
|
g.current_user = {'user_id': user['_id'],
|
|
'groups': user['groups'],
|
|
'roles': set(user['roles'])}
|
|
|
|
projects.create_project(overrides={'url': 'default-project',
|
|
'is_private': False})
|
|
|
|
|
|
@manager.command
|
|
def setup_db_indices():
|
|
"""Adds missing database indices."""
|
|
|
|
from application import setup_db_indices
|
|
|
|
import pymongo
|
|
|
|
log.info('Adding missing database indices.')
|
|
log.warning('This does NOT drop and recreate existing indices, '
|
|
'nor does it reconfigure existing indices. '
|
|
'If you want that, drop them manually first.')
|
|
|
|
setup_db_indices()
|
|
|
|
coll_names = db.collection_names(include_system_collections=False)
|
|
for coll_name in sorted(coll_names):
|
|
stats = db.command('collStats', coll_name)
|
|
log.info('Collection %25s takes up %.3f MiB index space',
|
|
coll_name, stats['totalIndexSize'] / 2**20)
|
|
|
|
|
|
def _default_permissions():
|
|
"""Returns a dict of default permissions.
|
|
|
|
Usable for projects, node types, and others.
|
|
|
|
:rtype: dict
|
|
"""
|
|
|
|
from application.modules.projects import DEFAULT_ADMIN_GROUP_PERMISSIONS
|
|
|
|
groups_collection = app.data.driver.db['groups']
|
|
admin_group = groups_collection.find_one({'name': 'admin'})
|
|
|
|
default_permissions = {
|
|
'world': ['GET'],
|
|
'users': [],
|
|
'groups': [
|
|
{'group': admin_group['_id'],
|
|
'methods': DEFAULT_ADMIN_GROUP_PERMISSIONS[:]},
|
|
]
|
|
}
|
|
|
|
return default_permissions
|
|
|
|
|
|
@manager.command
|
|
def setup_for_attract(project_uuid, replace=False):
|
|
"""Adds Attract node types to the project.
|
|
|
|
:param project_uuid: the UUID of the project to update
|
|
:type project_uuid: str
|
|
:param replace: whether to replace existing Attract node types (True),
|
|
or to keep existing node types (False, the default).
|
|
:type replace: bool
|
|
"""
|
|
|
|
from manage_extra.node_types.act import node_type_act
|
|
from manage_extra.node_types.scene import node_type_scene
|
|
from manage_extra.node_types.shot import node_type_shot
|
|
|
|
# Copy permissions from the project, then give everyone with PUT
|
|
# access also DELETE access.
|
|
project = _get_project(project_uuid)
|
|
permissions = copy.deepcopy(project['permissions'])
|
|
|
|
for perms in permissions.values():
|
|
for perm in perms:
|
|
methods = set(perm['methods'])
|
|
if 'PUT' not in perm['methods']:
|
|
continue
|
|
methods.add('DELETE')
|
|
perm['methods'] = list(methods)
|
|
|
|
node_type_act['permissions'] = permissions
|
|
node_type_scene['permissions'] = permissions
|
|
node_type_shot['permissions'] = permissions
|
|
|
|
# Add the missing node types.
|
|
for node_type in (node_type_act, node_type_scene, node_type_shot):
|
|
found = [nt for nt in project['node_types']
|
|
if nt['name'] == node_type['name']]
|
|
if found:
|
|
assert len(found) == 1, 'node type name should be unique (found %ix)' % len(found)
|
|
|
|
# TODO: validate that the node type contains all the properties Attract needs.
|
|
if replace:
|
|
log.info('Replacing existing node type %s', node_type['name'])
|
|
project['node_types'].remove(found[0])
|
|
else:
|
|
continue
|
|
|
|
project['node_types'].append(node_type)
|
|
|
|
_update_project(project_uuid, project)
|
|
|
|
log.info('Project %s was updated for Attract.', project_uuid)
|
|
|
|
|
|
def _get_project(project_uuid):
|
|
"""Find a project in the database, or SystemExit()s.
|
|
|
|
:param project_uuid: UUID of the project
|
|
:type: str
|
|
:return: the project
|
|
:rtype: dict
|
|
"""
|
|
|
|
projects_collection = app.data.driver.db['projects']
|
|
project_id = ObjectId(project_uuid)
|
|
|
|
# Find the project in the database.
|
|
project = projects_collection.find_one(project_id)
|
|
if not project:
|
|
log.error('Project %s does not exist.', project_uuid)
|
|
raise SystemExit()
|
|
|
|
return project
|
|
|
|
|
|
def _update_project(project_uuid, project):
|
|
"""Updates a project in the database, or SystemExit()s.
|
|
|
|
:param project_uuid: UUID of the project
|
|
:type: str
|
|
:param project: the project data, should be the entire project document
|
|
:type: dict
|
|
:return: the project
|
|
:rtype: dict
|
|
"""
|
|
|
|
from application.utils import remove_private_keys
|
|
|
|
project_id = ObjectId(project_uuid)
|
|
project = remove_private_keys(project)
|
|
result, _, _, _ = put_internal('projects', project, _id=project_id)
|
|
|
|
if result['_status'] != 'OK':
|
|
log.error("Can't update project %s, issues: %s", project_uuid, result['_issues'])
|
|
raise SystemExit()
|
|
|
|
|
|
@manager.command
|
|
def refresh_project_permissions():
|
|
"""Replaces the admin group permissions of each project with the defaults."""
|
|
|
|
from application.modules.projects import DEFAULT_ADMIN_GROUP_PERMISSIONS
|
|
|
|
proj_coll = app.data.driver.db['projects']
|
|
result = proj_coll.update_many({}, {'$set': {
|
|
'permissions.groups.0.methods': DEFAULT_ADMIN_GROUP_PERMISSIONS
|
|
}})
|
|
|
|
print('Matched %i documents' % result.matched_count)
|
|
print('Updated %i documents' % result.modified_count)
|
|
|
|
|
|
@manager.command
|
|
def clear_db():
|
|
"""Wipes the database
|
|
"""
|
|
from pymongo import MongoClient
|
|
|
|
client = MongoClient(MONGO_HOST, 27017)
|
|
db = client.eve
|
|
db.drop_collection('nodes')
|
|
db.drop_collection('node_types')
|
|
db.drop_collection('tokens')
|
|
db.drop_collection('users')
|
|
|
|
|
|
@manager.command
|
|
def add_parent_to_nodes():
|
|
"""Find the parent of any node in the nodes collection"""
|
|
import codecs
|
|
import sys
|
|
|
|
UTF8Writer = codecs.getwriter('utf8')
|
|
sys.stdout = UTF8Writer(sys.stdout)
|
|
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
|
|
def find_parent_project(node):
|
|
if node and 'parent' in node:
|
|
parent = nodes_collection.find_one({'_id': node['parent']})
|
|
return find_parent_project(parent)
|
|
if node:
|
|
return node
|
|
else:
|
|
return None
|
|
|
|
nodes = nodes_collection.find()
|
|
nodes_index = 0
|
|
nodes_orphan = 0
|
|
for node in nodes:
|
|
nodes_index += 1
|
|
if node['node_type'] == ObjectId("55a615cfea893bd7d0489f2d"):
|
|
print(u"Skipping project node - {0}".format(node['name']))
|
|
else:
|
|
project = find_parent_project(node)
|
|
if project:
|
|
nodes_collection.update({'_id': node['_id']},
|
|
{"$set": {'project': project['_id']}})
|
|
print(u"{0} {1}".format(node['_id'], node['name']))
|
|
else:
|
|
nodes_orphan += 1
|
|
nodes_collection.remove({'_id': node['_id']})
|
|
print("Removed {0} {1}".format(node['_id'], node['name']))
|
|
|
|
print("Edited {0} nodes".format(nodes_index))
|
|
print("Orphan {0} nodes".format(nodes_orphan))
|
|
|
|
|
|
@manager.command
|
|
def make_project_public(project_id):
|
|
"""Convert every node of a project from pending to public"""
|
|
|
|
DRY_RUN = False
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
for n in nodes_collection.find({'project': ObjectId(project_id)}):
|
|
n['properties']['status'] = 'published'
|
|
print(u"Publishing {0} {1}".format(n['_id'], n['name'].encode('ascii', 'ignore')))
|
|
if not DRY_RUN:
|
|
put_item('nodes', n)
|
|
|
|
|
|
@manager.command
|
|
def set_attachment_names():
|
|
"""Loop through all existing nodes and assign proper ContentDisposition
|
|
metadata to referenced files that are using GCS.
|
|
"""
|
|
from application.utils.gcs import update_file_name
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
for n in nodes_collection.find():
|
|
print("Updating node {0}".format(n['_id']))
|
|
update_file_name(n)
|
|
|
|
|
|
@manager.command
|
|
def files_verify_project():
|
|
"""Verify for missing or conflicting node/file ids"""
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
files_collection = app.data.driver.db['files']
|
|
issues = dict(missing=[], conflicting=[], processing=[])
|
|
|
|
def _parse_file(item, file_id):
|
|
f = files_collection.find_one({'_id': file_id})
|
|
if f:
|
|
if 'project' in item and 'project' in f:
|
|
if item['project'] != f['project']:
|
|
issues['conflicting'].append(item['_id'])
|
|
if 'status' in item['properties'] \
|
|
and item['properties']['status'] == 'processing':
|
|
issues['processing'].append(item['_id'])
|
|
else:
|
|
issues['missing'].append(
|
|
"{0} missing {1}".format(item['_id'], file_id))
|
|
|
|
for item in nodes_collection.find():
|
|
print("Verifying node {0}".format(item['_id']))
|
|
if 'file' in item['properties']:
|
|
_parse_file(item, item['properties']['file'])
|
|
elif 'files' in item['properties']:
|
|
for f in item['properties']['files']:
|
|
_parse_file(item, f['file'])
|
|
|
|
print("===")
|
|
print("Issues detected:")
|
|
for k, v in issues.iteritems():
|
|
print("{0}:".format(k))
|
|
for i in v:
|
|
print(i)
|
|
print("===")
|
|
|
|
|
|
def replace_node_type(project, node_type_name, new_node_type):
|
|
"""Update or create the specified node type. We rely on the fact that
|
|
node_types have a unique name in a project.
|
|
"""
|
|
|
|
old_node_type = next(
|
|
(item for item in project['node_types'] if item.get('name') \
|
|
and item['name'] == node_type_name), None)
|
|
if old_node_type:
|
|
for i, v in enumerate(project['node_types']):
|
|
if v['name'] == node_type_name:
|
|
project['node_types'][i] = new_node_type
|
|
else:
|
|
project['node_types'].append(new_node_type)
|
|
|
|
|
|
@manager.command
|
|
def project_upgrade_node_types(project_id):
|
|
projects_collection = app.data.driver.db['projects']
|
|
project = projects_collection.find_one({'_id': ObjectId(project_id)})
|
|
replace_node_type(project, 'group', node_type_group)
|
|
replace_node_type(project, 'asset', node_type_asset)
|
|
replace_node_type(project, 'storage', node_type_storage)
|
|
replace_node_type(project, 'comment', node_type_comment)
|
|
replace_node_type(project, 'blog', node_type_blog)
|
|
replace_node_type(project, 'post', node_type_post)
|
|
replace_node_type(project, 'texture', node_type_texture)
|
|
put_item('projects', project)
|
|
|
|
|
|
@manager.command
|
|
def test_put_item(node_id):
|
|
import pprint
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
node = nodes_collection.find_one(ObjectId(node_id))
|
|
pprint.pprint(node)
|
|
put_item('nodes', node)
|
|
|
|
|
|
@manager.command
|
|
def test_post_internal(node_id):
|
|
import pprint
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
node = nodes_collection.find_one(ObjectId(node_id))
|
|
internal_fields = ['_id', '_etag', '_updated', '_created']
|
|
for field in internal_fields:
|
|
node.pop(field, None)
|
|
pprint.pprint(node)
|
|
print(post_internal('nodes', node))
|
|
|
|
|
|
@manager.command
|
|
def algolia_push_users():
|
|
"""Loop through all users and push them to Algolia"""
|
|
from application.utils.algolia import algolia_index_user_save
|
|
users_collection = app.data.driver.db['users']
|
|
for user in users_collection.find():
|
|
print("Pushing {0}".format(user['username']))
|
|
algolia_index_user_save(user)
|
|
|
|
|
|
@manager.command
|
|
def algolia_push_nodes():
|
|
"""Loop through all nodes and push them to Algolia"""
|
|
from application.utils.algolia import algolia_index_node_save
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
for node in nodes_collection.find():
|
|
print(u"Pushing {0}: {1}".format(node['_id'], node['name'].encode(
|
|
'ascii', 'ignore')))
|
|
algolia_index_node_save(node)
|
|
|
|
|
|
@manager.command
|
|
def files_make_public_t():
|
|
"""Loop through all files and if they are images on GCS, make the size t
|
|
public
|
|
"""
|
|
from gcloud.exceptions import InternalServerError
|
|
from application.utils.gcs import GoogleCloudStorageBucket
|
|
files_collection = app.data.driver.db['files']
|
|
|
|
for f in files_collection.find({'backend': 'gcs'}):
|
|
if 'variations' not in f:
|
|
continue
|
|
|
|
variation_t = next((item for item in f['variations']
|
|
if item['size'] == 't'), None)
|
|
if not variation_t:
|
|
continue
|
|
|
|
try:
|
|
storage = GoogleCloudStorageBucket(str(f['project']))
|
|
blob = storage.Get(variation_t['file_path'], to_dict=False)
|
|
if not blob:
|
|
print('Unable to find blob for project %s file %s' % (f['project'], f['_id']))
|
|
continue
|
|
|
|
print('Making blob public: {0}'.format(blob.path))
|
|
blob.make_public()
|
|
except InternalServerError as ex:
|
|
print('Internal Server Error: ', ex)
|
|
|
|
|
|
@manager.command
|
|
def subscribe_node_owners():
|
|
"""Automatically subscribe node owners to notifications for items created
|
|
in the past.
|
|
"""
|
|
from application.modules.nodes import after_inserting_nodes
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
for n in nodes_collection.find():
|
|
if 'parent' in n:
|
|
after_inserting_nodes([n])
|
|
|
|
|
|
@manager.command
|
|
def refresh_project_links(project, chunk_size=50, quiet=False):
|
|
"""Regenerates almost-expired file links for a certain project."""
|
|
|
|
if quiet:
|
|
import logging
|
|
from application import log
|
|
|
|
logging.getLogger().setLevel(logging.WARNING)
|
|
log.setLevel(logging.WARNING)
|
|
|
|
chunk_size = int(chunk_size) # CLI parameters are passed as strings
|
|
from application.modules import file_storage
|
|
file_storage.refresh_links_for_project(project, chunk_size, 2 * 3600)
|
|
|
|
|
|
@manager.command
|
|
def refresh_backend_links(backend_name, chunk_size=50, quiet=False):
|
|
"""Refreshes all file links that are using a certain storage backend."""
|
|
|
|
if quiet:
|
|
import logging
|
|
from application import log
|
|
|
|
logging.getLogger().setLevel(logging.WARNING)
|
|
log.setLevel(logging.WARNING)
|
|
|
|
chunk_size = int(chunk_size) # CLI parameters are passed as strings
|
|
from application.modules import file_storage
|
|
file_storage.refresh_links_for_backend(backend_name, chunk_size, 2 * 3600)
|
|
|
|
|
|
@manager.command
|
|
def expire_all_project_links(project_uuid):
|
|
"""Expires all file links for a certain project without refreshing.
|
|
|
|
This is just for testing.
|
|
"""
|
|
|
|
import datetime
|
|
import bson.tz_util
|
|
|
|
files_collection = app.data.driver.db['files']
|
|
|
|
now = datetime.datetime.now(tz=bson.tz_util.utc)
|
|
expires = now - datetime.timedelta(days=1)
|
|
|
|
result = files_collection.update_many(
|
|
{'project': ObjectId(project_uuid)},
|
|
{'$set': {'link_expires': expires}}
|
|
)
|
|
|
|
print('Expired %i links' % result.matched_count)
|
|
|
|
|
|
@manager.command
|
|
def register_local_user(email, password):
|
|
from application.modules.local_auth import create_local_user
|
|
create_local_user(email, password)
|
|
|
|
|
|
@manager.command
|
|
def add_group_to_projects(group_name):
|
|
"""Prototype to add a specific group, in read-only mode, to all node_types
|
|
for all projects.
|
|
"""
|
|
methods = ['GET']
|
|
groups_collection = app.data.driver.db['groups']
|
|
projects_collections = app.data.driver.db['projects']
|
|
group = groups_collection.find_one({'name': group_name})
|
|
for project in projects_collections.find():
|
|
print("Processing: {}".format(project['name']))
|
|
for node_type in project['node_types']:
|
|
node_type_name = node_type['name']
|
|
base_node_types = ['group', 'asset', 'blog', 'post', 'page',
|
|
'comment', 'group_texture', 'storage', 'texture']
|
|
if node_type_name in base_node_types:
|
|
print("Processing: {0}".format(node_type_name))
|
|
# Check if group already exists in the permissions
|
|
g = next((g for g in node_type['permissions']['groups']
|
|
if g['group'] == group['_id']), None)
|
|
# If not, we add it
|
|
if g is None:
|
|
print("Adding permissions")
|
|
permissions = {
|
|
'group': group['_id'],
|
|
'methods': methods}
|
|
node_type['permissions']['groups'].append(permissions)
|
|
projects_collections.update(
|
|
{'_id': project['_id']}, project)
|
|
|
|
|
|
@manager.command
|
|
def add_license_props():
|
|
"""Add license fields to all node types asset for every project."""
|
|
projects_collections = app.data.driver.db['projects']
|
|
for project in projects_collections.find():
|
|
print("Processing {}".format(project['_id']))
|
|
for node_type in project['node_types']:
|
|
if node_type['name'] == 'asset':
|
|
node_type['dyn_schema']['license_notes'] = {'type': 'string'}
|
|
node_type['dyn_schema']['license_type'] = {
|
|
'type': 'string',
|
|
'allowed': [
|
|
'cc-by',
|
|
'cc-0',
|
|
'cc-by-sa',
|
|
'cc-by-nd',
|
|
'cc-by-nc',
|
|
'copyright'
|
|
],
|
|
'default': 'cc-by'
|
|
}
|
|
node_type['form_schema']['license_notes'] = {}
|
|
node_type['form_schema']['license_type'] = {}
|
|
projects_collections.update(
|
|
{'_id': project['_id']}, project)
|
|
|
|
|
|
@manager.command
|
|
def refresh_file_sizes():
|
|
"""Computes & stores the 'length_aggregate_in_bytes' fields of all files."""
|
|
|
|
from application.modules import file_storage
|
|
|
|
matched = 0
|
|
unmatched = 0
|
|
total_size = 0
|
|
|
|
files_collection = app.data.driver.db['files']
|
|
for file_doc in files_collection.find():
|
|
file_storage.compute_aggregate_length(file_doc)
|
|
length = file_doc['length_aggregate_in_bytes']
|
|
total_size += length
|
|
|
|
result = files_collection.update_one({'_id': file_doc['_id']},
|
|
{'$set': {'length_aggregate_in_bytes': length}})
|
|
if result.matched_count != 1:
|
|
log.warning('Unable to update document %s', file_doc['_id'])
|
|
unmatched += 1
|
|
else:
|
|
matched += 1
|
|
|
|
log.info('Updated %i file documents.', matched)
|
|
if unmatched:
|
|
log.warning('Unable to update %i documents.', unmatched)
|
|
log.info('%i bytes (%.3f GiB) storage used in total.',
|
|
total_size, total_size / 1024 ** 3)
|
|
|
|
|
|
@manager.command
|
|
def project_stats():
|
|
import csv
|
|
import sys
|
|
from collections import defaultdict
|
|
from functools import partial
|
|
|
|
from application.modules import projects
|
|
|
|
proj_coll = app.data.driver.db['projects']
|
|
nodes = app.data.driver.db['nodes']
|
|
|
|
aggr = defaultdict(partial(defaultdict, int))
|
|
|
|
csvout = csv.writer(sys.stdout)
|
|
csvout.writerow(['project ID', 'owner', 'private', 'file size',
|
|
'nr of nodes', 'nr of top-level nodes', ])
|
|
|
|
for proj in proj_coll.find(projection={'user': 1,
|
|
'name': 1,
|
|
'is_private': 1,
|
|
'_id': 1}):
|
|
project_id = proj['_id']
|
|
is_private = proj.get('is_private', False)
|
|
row = [str(project_id),
|
|
unicode(proj['user']).encode('utf-8'),
|
|
is_private]
|
|
|
|
file_size = projects.project_total_file_size(project_id)
|
|
row.append(file_size)
|
|
|
|
node_count_result = nodes.aggregate([
|
|
{'$match': {'project': project_id}},
|
|
{'$project': {'parent': 1,
|
|
'is_top': {'$cond': [{'$gt': ['$parent', None]}, 0, 1]},
|
|
}},
|
|
{'$group': {
|
|
'_id': None,
|
|
'all': {'$sum': 1},
|
|
'top': {'$sum': '$is_top'},
|
|
}}
|
|
])
|
|
|
|
try:
|
|
node_counts = next(node_count_result)
|
|
nodes_all = node_counts['all']
|
|
nodes_top = node_counts['top']
|
|
except StopIteration:
|
|
# No result from the nodes means nodeless project.
|
|
nodes_all = 0
|
|
nodes_top = 0
|
|
row.append(nodes_all)
|
|
row.append(nodes_top)
|
|
|
|
for collection in aggr[None], aggr[is_private]:
|
|
collection['project_count'] += 1
|
|
collection['project_count'] += 1
|
|
collection['file_size'] += file_size
|
|
collection['node_count'] += nodes_all
|
|
collection['top_nodes'] += nodes_top
|
|
|
|
csvout.writerow(row)
|
|
|
|
csvout.writerow([
|
|
'public', '', '%i projects' % aggr[False]['project_count'],
|
|
aggr[False]['file_size'], aggr[False]['node_count'], aggr[False]['top_nodes'],
|
|
])
|
|
csvout.writerow([
|
|
'private', '', '%i projects' % aggr[True]['project_count'],
|
|
aggr[True]['file_size'], aggr[True]['node_count'], aggr[True]['top_nodes'],
|
|
])
|
|
csvout.writerow([
|
|
'total', '', '%i projects' % aggr[None]['project_count'],
|
|
aggr[None]['file_size'], aggr[None]['node_count'], aggr[None]['top_nodes'],
|
|
])
|
|
|
|
|
|
@manager.command
|
|
def add_node_types():
|
|
"""Add texture and group_texture node types to all projects"""
|
|
from manage_extra.node_types.texture import node_type_texture
|
|
from manage_extra.node_types.group_texture import node_type_group_texture
|
|
from application.utils import project_get_node_type
|
|
projects_collections = app.data.driver.db['projects']
|
|
for project in projects_collections.find():
|
|
print("Processing {}".format(project['_id']))
|
|
if not project_get_node_type(project, 'group_texture'):
|
|
project['node_types'].append(node_type_group_texture)
|
|
print("Added node type: {}".format(node_type_group_texture['name']))
|
|
if not project_get_node_type(project, 'texture'):
|
|
project['node_types'].append(node_type_texture)
|
|
print("Added node type: {}".format(node_type_texture['name']))
|
|
projects_collections.update(
|
|
{'_id': project['_id']}, project)
|
|
|
|
|
|
@manager.command
|
|
def update_texture_node_type():
|
|
"""Update allowed values for textures node_types"""
|
|
projects_collections = app.data.driver.db['projects']
|
|
for project in projects_collections.find():
|
|
print("Processing {}".format(project['_id']))
|
|
for node_type in project['node_types']:
|
|
if node_type['name'] == 'texture':
|
|
allowed = [
|
|
'color',
|
|
'specular',
|
|
'bump',
|
|
'normal',
|
|
'translucency',
|
|
'emission',
|
|
'alpha'
|
|
]
|
|
node_type['dyn_schema']['files']['schema']['schema']['map_type'][
|
|
'allowed'] = allowed
|
|
projects_collections.update(
|
|
{'_id': project['_id']}, project)
|
|
|
|
|
|
@manager.command
|
|
def update_texture_nodes_maps():
|
|
"""Update abbreviated texture map types to the extended version"""
|
|
nodes_collection = app.data.driver.db['nodes']
|
|
remap = {
|
|
'col': 'color',
|
|
'spec': 'specular',
|
|
'nor': 'normal'}
|
|
for node in nodes_collection.find({'node_type': 'texture'}):
|
|
for v in node['properties']['files']:
|
|
try:
|
|
updated_map_types = remap[v['map_type']]
|
|
print("Updating {} to {}".format(v['map_type'], updated_map_types))
|
|
v['map_type'] = updated_map_types
|
|
except KeyError:
|
|
print("Skipping {}".format(v['map_type']))
|
|
nodes_collection.update({'_id': node['_id']}, node)
|
|
|
|
|
|
@manager.command
|
|
def create_badger_account(email, badges):
|
|
"""
|
|
Creates a new service account that can give badges (i.e. roles).
|
|
|
|
:param email: email address associated with the account
|
|
:param badges: single space-separated argument containing the roles
|
|
this account can assign and revoke.
|
|
"""
|
|
|
|
from application.modules import service
|
|
from application.utils import dumps
|
|
|
|
account, token = service.create_service_account(
|
|
email,
|
|
[u'badger'],
|
|
{'badger': badges.strip().split()}
|
|
)
|
|
|
|
print('Account created:')
|
|
print(dumps(account, indent=4, sort_keys=True))
|
|
print()
|
|
print('Access token: %s' % token['token'])
|
|
print(' expires on: %s' % token['expire_time'])
|
|
|
|
|
|
@manager.command
|
|
def find_duplicate_users():
|
|
"""Finds users that have the same BlenderID user_id."""
|
|
|
|
from collections import defaultdict
|
|
|
|
users_coll = app.data.driver.db['users']
|
|
nodes_coll = app.data.driver.db['nodes']
|
|
projects_coll = app.data.driver.db['projects']
|
|
|
|
found_users = defaultdict(list)
|
|
|
|
for user in users_coll.find():
|
|
blender_ids = [auth['user_id'] for auth in user['auth']
|
|
if auth['provider'] == 'blender-id']
|
|
if not blender_ids:
|
|
continue
|
|
blender_id = blender_ids[0]
|
|
found_users[blender_id].append(user)
|
|
|
|
for blender_id, users in found_users.iteritems():
|
|
if len(users) == 1:
|
|
continue
|
|
|
|
usernames = ', '.join(user['username'] for user in users)
|
|
print('Blender ID: %5s has %i users: %s' % (
|
|
blender_id, len(users), usernames))
|
|
|
|
for user in users:
|
|
print(' %s owns %i nodes and %i projects' % (
|
|
user['username'],
|
|
nodes_coll.count({'user': user['_id']}),
|
|
projects_coll.count({'user': user['_id']}),
|
|
))
|
|
|
|
|
|
@manager.command
|
|
def sync_role_groups(do_revoke_groups):
|
|
"""For each user, synchronizes roles and group membership.
|
|
|
|
This ensures that everybody with the 'subscriber' role is also member of the 'subscriber'
|
|
group, and people without the 'subscriber' role are not member of that group. Same for
|
|
admin and demo groups.
|
|
|
|
When do_revoke_groups=False (the default), people are only added to groups.
|
|
when do_revoke_groups=True, people are also removed from groups.
|
|
"""
|
|
|
|
from application.modules import service
|
|
|
|
if do_revoke_groups not in {'true', 'false'}:
|
|
print('Use either "true" or "false" as first argument.')
|
|
print('When passing "false", people are only added to groups.')
|
|
print('when passing "true", people are also removed from groups.')
|
|
raise SystemExit()
|
|
do_revoke_groups = do_revoke_groups == 'true'
|
|
|
|
service.fetch_role_to_group_id_map()
|
|
|
|
users_coll = app.data.driver.db['users']
|
|
groups_coll = app.data.driver.db['groups']
|
|
|
|
group_names = {}
|
|
|
|
def gname(gid):
|
|
try:
|
|
return group_names[gid]
|
|
except KeyError:
|
|
name = groups_coll.find_one(gid, projection={'name': 1})['name']
|
|
name = str(name)
|
|
group_names[gid] = name
|
|
return name
|
|
|
|
ok_users = bad_users = 0
|
|
for user in users_coll.find():
|
|
grant_groups = set()
|
|
revoke_groups = set()
|
|
current_groups = set(user.get('groups', []))
|
|
user_roles = user.get('roles', set())
|
|
|
|
for role in service.ROLES_WITH_GROUPS:
|
|
action = 'grant' if role in user_roles else 'revoke'
|
|
groups = service.manage_user_group_membership(user, role, action)
|
|
|
|
if groups is None:
|
|
# No changes required
|
|
continue
|
|
|
|
if groups == current_groups:
|
|
continue
|
|
|
|
grant_groups.update(groups.difference(current_groups))
|
|
revoke_groups.update(current_groups.difference(groups))
|
|
|
|
if grant_groups or revoke_groups:
|
|
bad_users += 1
|
|
|
|
expected_groups = current_groups.union(grant_groups).difference(revoke_groups)
|
|
|
|
print('Discrepancy for user %s/%s:' % (user['_id'], user['full_name'].encode('utf8')))
|
|
print(' - actual groups :', sorted(gname(gid) for gid in user.get('groups')))
|
|
print(' - expected groups:', sorted(gname(gid) for gid in expected_groups))
|
|
print(' - will grant :', sorted(gname(gid) for gid in grant_groups))
|
|
|
|
if do_revoke_groups:
|
|
label = 'WILL REVOKE '
|
|
else:
|
|
label = 'could revoke'
|
|
print(' - %s :' % label, sorted(gname(gid) for gid in revoke_groups))
|
|
|
|
if grant_groups and revoke_groups:
|
|
print(' ------ CAREFUL this one has BOTH grant AND revoke -----')
|
|
|
|
# Determine which changes we'll apply
|
|
final_groups = current_groups.union(grant_groups)
|
|
if do_revoke_groups:
|
|
final_groups.difference_update(revoke_groups)
|
|
print(' - final groups :', sorted(gname(gid) for gid in final_groups))
|
|
|
|
# Perform the actual update
|
|
users_coll.update_one({'_id': user['_id']},
|
|
{'$set': {'groups': list(final_groups)}})
|
|
else:
|
|
ok_users += 1
|
|
|
|
print('%i bad and %i ok users seen.' % (bad_users, ok_users))
|
|
|
|
|
|
@manager.command
|
|
def sync_project_groups(user_email, fix):
|
|
"""Gives the user access to their self-created projects."""
|
|
|
|
if fix.lower() not in {'true', 'false'}:
|
|
print('Use either "true" or "false" as second argument.')
|
|
print('When passing "false", only a report is produced.')
|
|
print('when passing "true", group membership is fixed.')
|
|
raise SystemExit()
|
|
fix = fix.lower() == 'true'
|
|
|
|
users_coll = app.data.driver.db['users']
|
|
proj_coll = app.data.driver.db['projects']
|
|
groups_coll = app.data.driver.db['groups']
|
|
|
|
# Find by email or by user ID
|
|
if '@' in user_email:
|
|
where = {'email': user_email}
|
|
else:
|
|
try:
|
|
where = {'_id': ObjectId(user_email)}
|
|
except InvalidId:
|
|
log.warning('Invalid ObjectID: %s', user_email)
|
|
return
|
|
|
|
user = users_coll.find_one(where, projection={'_id': 1, 'groups': 1})
|
|
if user is None:
|
|
log.error('User %s not found', where)
|
|
raise SystemExit()
|
|
|
|
user_groups = set(user['groups'])
|
|
user_id = user['_id']
|
|
log.info('Updating projects for user %s', user_id)
|
|
|
|
ok_groups = missing_groups = 0
|
|
for proj in proj_coll.find({'user': user_id}):
|
|
project_id = proj['_id']
|
|
log.info('Investigating project %s (%s)', project_id, proj['name'])
|
|
|
|
# Find the admin group
|
|
admin_group = groups_coll.find_one({'name': str(project_id)}, projection={'_id': 1})
|
|
if admin_group is None:
|
|
log.warning('No admin group for project %s', project_id)
|
|
continue
|
|
group_id = admin_group['_id']
|
|
|
|
# Check membership
|
|
if group_id not in user_groups:
|
|
log.info('Missing group membership')
|
|
missing_groups += 1
|
|
user_groups.add(group_id)
|
|
else:
|
|
ok_groups += 1
|
|
|
|
log.info('User %s was missing %i group memberships; %i projects were ok.',
|
|
user_id, missing_groups, ok_groups)
|
|
|
|
if missing_groups > 0 and fix:
|
|
log.info('Updating database.')
|
|
result = users_coll.update_one({'_id': user_id},
|
|
{'$set': {'groups': list(user_groups)}})
|
|
log.info('Updated %i user.', result.modified_count)
|
|
|
|
|
|
@manager.command
|
|
def badger(action, user_email, role):
|
|
from application.modules import service
|
|
|
|
with app.app_context():
|
|
service.fetch_role_to_group_id_map()
|
|
response, status = service.do_badger(action, user_email, role)
|
|
|
|
if status == 204:
|
|
log.info('Done.')
|
|
else:
|
|
log.info('Response: %s', response)
|
|
log.info('Status : %i', status)
|
|
|
|
if __name__ == '__main__':
|
|
manager.run()
|