diff --git a/doc/source/releases/17.01_announce.rst b/doc/source/releases/17.01_announce.rst index 97ae359e2a9..199dbbd68f4 100644 --- a/doc/source/releases/17.01_announce.rst +++ b/doc/source/releases/17.01_announce.rst @@ -29,15 +29,17 @@ Highlights Thanks to `@abretaud `__, `@ashvark `__, `@jvolkening `__, and `@mvdbeek `__. Implemented in `Pull Request 3145`_, `PullRequest 3510`_ and `PullRequest 3514`_. -`Github `__ -=========================================================== +Get Galaxy +========== -New Galaxy repository +The code lives at `Github `__ and you should have `Git `__ to obtain it. + +To get a new Galaxy repository run: .. code-block:: shell $ git clone -b release_17.01 https://github.com/galaxyproject/galaxy.git -Update of existing Galaxy repository +To update an existing Galaxy repository run: .. code-block:: shell $ git checkout release_17.01 && git pull --ff-only origin release_17.01 diff --git a/lib/galaxy/queue_worker.py b/lib/galaxy/queue_worker.py index abbde5ed55d..0ca7b928d77 100644 --- a/lib/galaxy/queue_worker.py +++ b/lib/galaxy/queue_worker.py @@ -9,6 +9,7 @@ import time import galaxy.queues from galaxy import util +from galaxy.model.util import pgcalc from kombu import Connection from kombu.mixins import ConsumerMixin @@ -18,7 +19,31 @@ logging.getLogger('kombu').setLevel(logging.WARNING) log = logging.getLogger(__name__) +def send_local_control_task(app, task, kwargs={}): + """ + This sends a message to the process-local control worker, which is useful + for one-time asynchronous tasks like recalculating user disk usage. + """ + log.info("Queuing async task %s." % task) + payload = {'task': task, + 'kwargs': kwargs} + try: + c = Connection(app.config.amqp_internal_connection) + with producers[c].acquire(block=True) as producer: + producer.publish(payload, + exchange=galaxy.queues.galaxy_exchange, + declare=[galaxy.queues.galaxy_exchange] + [galaxy.queues.control_queue_from_config(app.config)], + routing_key='control') + except Exception: + log.exception("Error queueing async task: %s." % payload) + + def send_control_task(app, task, noop_self=False, kwargs={}): + """ + This sends a control task out to all processes, useful for things like + reloading a data table, which needs to happen individually in all + processes. + """ log.info("Sending %s control task." % task) payload = {'task': task, 'kwargs': kwargs} @@ -112,6 +137,21 @@ def reload_sanitize_whitelist(app): app.config.reload_sanitize_whitelist() +def recalculate_user_disk_usage(app, **kwargs): + user_id = kwargs.get('user_id', None) + sa_session = app.model.context + if user_id: + user = sa_session.query( app.model.User ).get( app.security.decode_id( user_id ) ) + if user: + if sa_session.get_bind().dialect.name not in ( 'postgres', 'postgresql' ): + new = user.calculate_disk_usage() + else: + new = pgcalc(sa_session, user.id) + user.set_disk_usage(new) + sa_session.add(user) + sa_session.flush() + + def reload_tool_data_tables(app, **kwargs): params = util.Params(kwargs) log.debug("Executing tool data table reload for %s" % params.get('table_names', 'all tables')) @@ -135,7 +175,8 @@ control_message_to_task = { 'create_panel_section': create_panel_section, 'reload_display_application': reload_display_application, 'reload_tool_data_tables': reload_tool_data_tables, 'admin_job_lock': admin_job_lock, - 'reload_sanitize_whitelist': reload_sanitize_whitelist} + 'reload_sanitize_whitelist': reload_sanitize_whitelist, + 'recalculate_user_disk_usage': recalculate_user_disk_usage} class GalaxyQueueWorker(ConsumerMixin, threading.Thread): diff --git a/lib/galaxy/tools/search/__init__.py b/lib/galaxy/tools/search/__init__.py index 2e30f5d7845..489bc4d125f 100644 --- a/lib/galaxy/tools/search/__init__.py +++ b/lib/galaxy/tools/search/__init__.py @@ -49,8 +49,9 @@ class ToolBoxSearch( object ): self.build_index( index_help ) def build_index( self, index_help=True ): - # Works around https://bitbucket.org/mchaput/whoosh/issues/391/race-conditions-with-temp-storage + """Prepare search index for tools loaded in toolbox.""" RamStorage.temp_storage = _temp_storage + # Works around https://bitbucket.org/mchaput/whoosh/issues/391/race-conditions-with-temp-storage self.storage = RamStorage() self.index = self.storage.create_index( self.schema ) writer = self.index.writer() @@ -66,16 +67,15 @@ class ToolBoxSearch( object ): "section": to_unicode( tool.get_panel_section()[1] if len( tool.get_panel_section() ) == 2 else '' ), "help": to_unicode( "" ) } - # Hyphens are wildcards in Whoosh causing bad things if tool.name.find( '-' ) != -1: + # Hyphens are wildcards in Whoosh causing bad things add_doc_kwds['name'] = (' ').join( [ token.text for token in self.rex( to_unicode( tool.name ) ) ] ) else: add_doc_kwds['name'] = to_unicode( tool.name ) - # We do not want to search Tool Shed or version parts - # of the long ids - if id.find( '/' ) != -1: - slash_indexes = [ m.start() for m in re.finditer( '/', id ) ] - id_stub = id[ ( slash_indexes[1] + 1 ): slash_indexes[4] ] + if tool.guid: + # Create a stub consisting of owner, repo, and tool from guid + slash_indexes = [ m.start() for m in re.finditer( '/', tool.guid ) ] + id_stub = tool.guid[ ( slash_indexes[1] + 1 ): slash_indexes[4] ] add_doc_kwds['stub'] = (' ').join( [ token.text for token in self.rex( to_unicode( id_stub ) ) ] ) else: add_doc_kwds['stub'] = to_unicode( id ) diff --git a/lib/galaxy/webapps/galaxy/controllers/user.py b/lib/galaxy/webapps/galaxy/controllers/user.py index c69eb8765e0..471b20dee7c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -17,6 +17,7 @@ from galaxy import model from galaxy import util from galaxy import web from galaxy.exceptions import ObjectInvalid +from galaxy.queue_worker import send_local_control_task from galaxy.security.validate_user_input import (transform_publicname, validate_email, validate_password, @@ -624,9 +625,12 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat refresh_frames = [ 'masthead', 'history', 'tools' ] else: refresh_frames = [ 'masthead', 'history' ] - # Recalculate user disk usage. if trans.user: - trans.user.calculate_disk_usage() + # Queue a quota recalculation (async) task -- this takes a + # while sometimes, so we don't want to block on logout. + send_local_control_task( trans.app, + 'recalculate_user_disk_usage', + {'user_id': trans.security.encode_id(trans.user.id)} ) # Since logging an event requires a session, we'll log prior to ending the session trans.log_event( "User logged out" ) else: @@ -1190,3 +1194,209 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat message=message, display_top=kwd.get('redirect_home', False) ) + + @web.expose + @web.require_admin + def edit_info( self, trans, cntrller, **kwd ): + """ + TEMPORARY ENDPOINT - added back to support admin-level user info + editing prior to adminjs. This is code that was prematurely removed + from the user controller when the user-side editing functionality was + replaced. + + The method manage_user_info, which follows this, should also be removed + at that time. + + Edit user information = username, email or password. + """ + params = util.Params( kwd ) + is_admin = cntrller == 'admin' and trans.user_is_admin() + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + user_id = params.get( 'user_id', None ) + if user_id and is_admin: + user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( user_id ) ) + elif user_id and ( not trans.user or trans.user.id != trans.security.decode_id( user_id ) ): + message = 'Invalid user id' + status = 'error' + user = None + else: + user = trans.user + if user and params.get( 'login_info_button', False ): + # Editing email and username + email = util.restore_text( params.get( 'email', '' ) ) + username = util.restore_text( params.get( 'username', '' ) ).lower() + + # Validate the new values for email and username + message = validate_email( trans, email, user ) + if not message and username: + message = validate_publicname( trans, username, user ) + if message: + status = 'error' + else: + if ( user.email != email ): + # The user's private role name must match the user's login ( email ) + private_role = trans.app.security_agent.get_private_user_role( user ) + private_role.name = email + private_role.description = 'Private role for ' + email + # Change the email itself + user.email = email + trans.sa_session.add_all( ( user, private_role ) ) + trans.sa_session.flush() + if trans.webapp.name == 'galaxy' and trans.app.config.user_activation_on: + user.active = False + trans.sa_session.add( user ) + trans.sa_session.flush() + is_activation_sent = self.send_verification_email( trans, user.email, user.username ) + if is_activation_sent: + message = 'The login information has been updated with the changes.
Verification email has been sent to your new email address. Please verify it by clicking the activation link in the email.
Please check your spam/trash folder in case you cannot find the message.' + else: + message = 'Unable to send activation email, please contact your local Galaxy administrator.' + if trans.app.config.error_email_to is not None: + message += ' Contact: %s' % trans.app.config.error_email_to + if ( user.username != username ): + user.username = username + trans.sa_session.add( user ) + trans.sa_session.flush() + message = 'The login information has been updated with the changes.' + elif user and params.get( 'edit_user_info_button', False ): + # Edit user information - webapp MUST BE 'galaxy' + user_type_fd_id = params.get( 'user_type_fd_id', 'none' ) + if user_type_fd_id not in [ 'none' ]: + user_type_form_definition = trans.sa_session.query( trans.app.model.FormDefinition ).get( trans.security.decode_id( user_type_fd_id ) ) + elif user.values: + user_type_form_definition = user.values.form_definition + else: + # User was created before any of the user_info forms were created + user_type_form_definition = None + if user_type_form_definition: + values = self.get_form_values( trans, user, user_type_form_definition, **kwd ) + else: + values = {} + flush_needed = False + if user.values: + # Editing the user info of an existing user with existing user info + user.values.content = values + trans.sa_session.add( user.values ) + flush_needed = True + elif values: + form_values = trans.model.FormValues( user_type_form_definition, values ) + trans.sa_session.add( form_values ) + user.values = form_values + flush_needed = True + if flush_needed: + trans.sa_session.add( user ) + trans.sa_session.flush() + message = "The user information has been updated with the changes." + if user and trans.webapp.name == 'galaxy' and is_admin: + kwd[ 'user_id' ] = trans.security.encode_id( user.id ) + kwd[ 'id' ] = user_id + if message: + kwd[ 'message' ] = util.sanitize_text( message ) + if status: + kwd[ 'status' ] = status + return trans.response.send_redirect( web.url_for( controller='user', + action='manage_user_info', + cntrller=cntrller, + **kwd ) ) + + @web.expose + @web.require_admin + def manage_user_info( self, trans, cntrller, **kwd ): + '''TEMPORARY ENDPOINT - added back to support admin-level user info + editing prior to adminjs. This is code that was prematurely removed + from the user controller when the user-side editing functionality was + replaced. + + When this is removed, templates/webapps/galaxy/user/manage_info.mako + should go as well. + + Manage a user's login, password, public username, type, + addresses, etc.''' + + def __get_user_type_form_definition( trans, user=None, **kwd ): + params = util.Params( kwd ) + if user and user.values: + user_type_fd_id = trans.security.encode_id( user.values.form_definition.id ) + else: + user_type_fd_id = params.get( 'user_type_fd_id', 'none' ) + if user_type_fd_id not in [ 'none' ]: + user_type_form_definition = trans.sa_session.query( trans.app.model.FormDefinition ).get( trans.security.decode_id( user_type_fd_id ) ) + else: + user_type_form_definition = None + return user_type_form_definition + + def __get_widgets( trans, user_type_form_definition, user=None, **kwd ): + widgets = [] + if user_type_form_definition: + if user: + if user.values: + widgets = user_type_form_definition.get_widgets( user=user, + contents=user.values.content, + **kwd ) + else: + widgets = user_type_form_definition.get_widgets( None, contents={}, **kwd ) + else: + widgets = user_type_form_definition.get_widgets( None, contents={}, **kwd ) + return widgets + + def __build_user_type_fd_id_select_field( trans, selected_value ): + from galaxy.web.form_builder import build_select_field + # Get all the user information forms + user_info_forms = self.get_all_forms( trans, + filter=dict( deleted=False ), + form_type=trans.model.FormDefinition.types.USER_INFO ) + return build_select_field( trans, + objs=user_info_forms, + label_attr='name', + select_field_name='user_type_fd_id', + initial_value='none', + selected_value=selected_value, + refresh_on_change=True ) + + params = util.Params( kwd ) + user_id = params.get( 'id', None ) + if user_id: + user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( user_id ) ) + else: + user = trans.user + if not user: + raise AssertionError("The user id (%s) is not valid" % str( user_id )) + email = util.restore_text( params.get( 'email', user.email ) ) + username = util.restore_text( params.get( 'username', '' ) ) + if not username: + username = user.username + message = escape( util.restore_text( params.get( 'message', '' ) ) ) + status = params.get( 'status', 'done' ) + user_type_form_definition = __get_user_type_form_definition( trans, user=user, **kwd ) + user_type_fd_id = params.get( 'user_type_fd_id', 'none' ) + if user_type_fd_id == 'none' and user_type_form_definition is not None: + user_type_fd_id = trans.security.encode_id( user_type_form_definition.id ) + user_type_fd_id_select_field = __build_user_type_fd_id_select_field( trans, selected_value=user_type_fd_id ) + widgets = __get_widgets( trans, user_type_form_definition, user=user, **kwd ) + # user's addresses + show_filter = util.restore_text( params.get( 'show_filter', 'Active' ) ) + if show_filter == 'All': + addresses = [address for address in user.addresses] + elif show_filter == 'Deleted': + addresses = [address for address in user.addresses if address.deleted] + else: + addresses = [address for address in user.addresses if not address.deleted] + user_info_forms = self.get_all_forms( trans, + filter=dict( deleted=False ), + form_type=trans.app.model.FormDefinition.types.USER_INFO ) + return trans.fill_template( '/webapps/galaxy/user/manage_info.mako', + cntrller=cntrller, + user=user, + email=email, + is_admin=True, + username=username, + user_type_fd_id_select_field=user_type_fd_id_select_field, + user_info_forms=user_info_forms, + user_type_form_definition=user_type_form_definition, + user_type_fd_id=user_type_fd_id, + widgets=widgets, + addresses=addresses, + show_filter=show_filter, + message=message, + status=status ) diff --git a/templates/webapps/galaxy/user/manage_info.mako b/templates/webapps/galaxy/user/manage_info.mako new file mode 100644 index 00000000000..0342174cc52 --- /dev/null +++ b/templates/webapps/galaxy/user/manage_info.mako @@ -0,0 +1,40 @@ +<%inherit file="/base.mako"/> +<%namespace file="/user/info.mako" import="render_user_info" /> +<%namespace file="/message.mako" import="render_msg" /> + +%if message: + ${render_msg( message, status )} +%endif + +${render_user_info()} + +%if user.values or user_info_forms: +

+
+
+
User information
+ %if user_type_fd_id_select_field and len( user_type_fd_id_select_field.options ) >= 1: +
+ + ${user_type_fd_id_select_field.get_html()} +
+ %else: + + %endif + %for field in widgets: +
+ + ${field['widget'].get_html()} +
+ ${field['helptext']} +
+
+
+ %endfor +
+ +
+
+
+

+%endif