PageRenderTime 135ms CodeModel.GetById 14ms app.highlight 109ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/galaxy/webapps/community/controllers/repository.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 1144 lines | 1081 code | 7 blank | 56 comment | 56 complexity | aa433671131278cfec4260685f873f4f MD5 | raw file
   1import os, logging, urllib, ConfigParser, tempfile, shutil
   2from time import strftime
   3from datetime import date, datetime
   4from galaxy import util
   5from galaxy.datatypes.checkers import *
   6from galaxy.web.base.controller import *
   7from galaxy.web.form_builder import CheckboxField
   8from galaxy.webapps.community import model
   9from galaxy.webapps.community.model import directory_hash_id
  10from galaxy.web.framework.helpers import time_ago, iff, grids
  11from galaxy.util.json import from_json_string, to_json_string
  12from galaxy.model.orm import *
  13from common import *
  14from mercurial import hg, ui, patch, commands
  15
  16log = logging.getLogger( __name__ )
  17
  18# Characters that must be html escaped
  19MAPPED_CHARS = { '>' :'>', 
  20                 '<' :'&lt;',
  21                 '"' : '&quot;',
  22                 '&' : '&amp;',
  23                 '\'' : '&apos;' }
  24MAX_CONTENT_SIZE = 32768
  25VALID_CHARS = set( string.letters + string.digits + "'\"-=_.()/+*^,:?!#[]%\\$@;{}" )
  26VALID_REPOSITORYNAME_RE = re.compile( "^[a-z0-9\_]+$" )
  27    
  28class CategoryListGrid( grids.Grid ):
  29    class NameColumn( grids.TextColumn ):
  30        def get_value( self, trans, grid, category ):
  31            return category.name
  32    class DescriptionColumn( grids.TextColumn ):
  33        def get_value( self, trans, grid, category ):
  34            return category.description
  35    class RepositoriesColumn( grids.TextColumn ):
  36        def get_value( self, trans, grid, category ):
  37            if category.repositories:
  38                viewable_repositories = 0
  39                for rca in category.repositories:
  40                    viewable_repositories += 1
  41                return viewable_repositories
  42            return 0
  43
  44    # Grid definition
  45    webapp = "community"
  46    title = "Categories"
  47    model_class = model.Category
  48    template='/webapps/community/category/grid.mako'
  49    default_sort_key = "name"
  50    columns = [
  51        NameColumn( "Name",
  52                    key="Category.name",
  53                    link=( lambda item: dict( operation="repositories_by_category", id=item.id, webapp="community" ) ),
  54                    attach_popup=False ),
  55        DescriptionColumn( "Description",
  56                           key="Category.description",
  57                           attach_popup=False ),
  58        # Columns that are valid for filtering but are not visible.
  59        RepositoriesColumn( "Repositories",
  60                            model_class=model.Repository,
  61                            attach_popup=False )
  62    ]
  63    # Override these
  64    default_filter = {}
  65    global_actions = []
  66    operations = []
  67    standard_filters = []
  68    num_rows_per_page = 50
  69    preserve_state = False
  70    use_paging = True
  71
  72class RepositoryListGrid( grids.Grid ):
  73    class NameColumn( grids.TextColumn ):
  74        def get_value( self, trans, grid, repository ):
  75            return repository.name
  76    class RevisionColumn( grids.GridColumn ):
  77        def __init__( self, col_name ):
  78            grids.GridColumn.__init__( self, col_name )
  79        def get_value( self, trans, grid, repository ):
  80            """
  81            Display a SelectField whose options are the changeset_revision
  82            strings of all downloadable_revisions of this repository.
  83            """
  84            select_field = build_changeset_revision_select_field( trans, repository )
  85            if len( select_field.options ) > 1:
  86                return select_field.get_html()
  87            return repository.revision
  88    class DescriptionColumn( grids.TextColumn ):
  89        def get_value( self, trans, grid, repository ):
  90            return repository.description
  91    class CategoryColumn( grids.TextColumn ):
  92        def get_value( self, trans, grid, repository ):
  93            rval = '<ul>'
  94            if repository.categories:
  95                for rca in repository.categories:
  96                    rval += '<li><a href="browse_repositories?operation=repositories_by_category&id=%s&webapp=community">%s</a></li>' \
  97                        % ( trans.security.encode_id( rca.category.id ), rca.category.name )
  98            else:
  99                rval += '<li>not set</li>'
 100            rval += '</ul>'
 101            return rval
 102    class RepositoryCategoryColumn( grids.GridColumn ):
 103        def filter( self, trans, user, query, column_filter ):
 104            """Modify query to filter by category."""
 105            if column_filter == "All":
 106                return query
 107            return query.filter( model.Category.name == column_filter )
 108    class UserColumn( grids.TextColumn ):
 109        def get_value( self, trans, grid, repository ):
 110            if repository.user:
 111                return repository.user.username
 112            return 'no user'
 113    class EmailColumn( grids.TextColumn ):
 114        def filter( self, trans, user, query, column_filter ):
 115            if column_filter == 'All':
 116                return query
 117            return query.filter( and_( model.Repository.table.c.user_id == model.User.table.c.id,
 118                                       model.User.table.c.email == column_filter ) )
 119    class EmailAlertsColumn( grids.TextColumn ):
 120        def get_value( self, trans, grid, repository ):
 121            if trans.user and repository.email_alerts and trans.user.email in from_json_string( repository.email_alerts ):
 122                return 'yes'
 123            return ''
 124    # Grid definition
 125    title = "Repositories"
 126    model_class = model.Repository
 127    template='/webapps/community/repository/grid.mako'
 128    default_sort_key = "name"
 129    columns = [
 130        NameColumn( "Name",
 131                    key="name",
 132                    link=( lambda item: dict( operation="view_or_manage_repository",
 133                                              id=item.id,
 134                                              webapp="community" ) ),
 135                    attach_popup=True ),
 136        DescriptionColumn( "Synopsis",
 137                           key="description",
 138                           attach_popup=False ),
 139        RevisionColumn( "Revision" ),
 140        CategoryColumn( "Category",
 141                        model_class=model.Category,
 142                        key="Category.name",
 143                        attach_popup=False ),
 144        UserColumn( "Owner",
 145                     model_class=model.User,
 146                     link=( lambda item: dict( operation="repositories_by_user", id=item.id, webapp="community" ) ),
 147                     attach_popup=False,
 148                     key="User.username" ),
 149        grids.CommunityRatingColumn( "Average Rating", key="rating" ),
 150        EmailAlertsColumn( "Alert", attach_popup=False ),
 151        # Columns that are valid for filtering but are not visible.
 152        EmailColumn( "Email",
 153                     model_class=model.User,
 154                     key="email",
 155                     visible=False ),
 156        RepositoryCategoryColumn( "Category",
 157                                  model_class=model.Category,
 158                                  key="Category.name",
 159                                  visible=False ),
 160        grids.DeletedColumn( "Deleted",
 161                             key="deleted",
 162                             visible=False,
 163                             filterable="advanced" )
 164    ]
 165    columns.append( grids.MulticolFilterColumn( "Search repository name, description", 
 166                                                cols_to_filter=[ columns[0], columns[1] ],
 167                                                key="free-text-search",
 168                                                visible=False,
 169                                                filterable="standard" ) )
 170    operations = [ grids.GridOperation( "Receive email alerts",
 171                                        allow_multiple=False,
 172                                        condition=( lambda item: not item.deleted ),
 173                                        async_compatible=False ) ]
 174    standard_filters = []
 175    default_filter = dict( deleted="False" )
 176    num_rows_per_page = 50
 177    preserve_state = False
 178    use_paging = True
 179    def build_initial_query( self, trans, **kwd ):
 180        return trans.sa_session.query( self.model_class ) \
 181                               .join( model.User.table ) \
 182                               .outerjoin( model.RepositoryCategoryAssociation.table ) \
 183                               .outerjoin( model.Category.table )
 184
 185class DownloadableRepositoryListGrid( RepositoryListGrid ):
 186    class RevisionColumn( grids.GridColumn ):
 187        def __init__( self, col_name ):
 188            grids.GridColumn.__init__( self, col_name )
 189        def get_value( self, trans, grid, repository ):
 190            """
 191            Display a SelectField whose options are the changeset_revision
 192            strings of all downloadable_revisions of this repository.
 193            """
 194            select_field = build_changeset_revision_select_field( trans, repository )
 195            if len( select_field.options ) > 1:
 196                return select_field.get_html()
 197            return repository.revision
 198    title = "Downloadable repositories"
 199    columns = [
 200        RepositoryListGrid.NameColumn( "Name",
 201                                       key="name",
 202                                       attach_popup=True ),
 203        RepositoryListGrid.DescriptionColumn( "Synopsis",
 204                                              key="description",
 205                                              attach_popup=False ),
 206        RevisionColumn( "Revision" ),
 207        RepositoryListGrid.UserColumn( "Owner",
 208                                       model_class=model.User,
 209                                       attach_popup=False,
 210                                       key="User.username" )
 211    ]
 212    columns.append( grids.MulticolFilterColumn( "Search repository name, description", 
 213                                                cols_to_filter=[ columns[0], columns[1] ],
 214                                                key="free-text-search",
 215                                                visible=False,
 216                                                filterable="standard" ) )
 217    operations = []
 218    def build_initial_query( self, trans, **kwd ):
 219        return trans.sa_session.query( self.model_class ) \
 220                               .join( model.RepositoryMetadata.table ) \
 221                               .join( model.User.table )
 222
 223class RepositoryController( BaseUIController, ItemRatings ):
 224
 225    downloadable_repository_list_grid = DownloadableRepositoryListGrid()
 226    repository_list_grid = RepositoryListGrid()
 227    category_list_grid = CategoryListGrid()
 228
 229    @web.expose
 230    def index( self, trans, **kwd ):
 231        params = util.Params( kwd )
 232        message = util.restore_text( params.get( 'message', ''  ) )
 233        status = params.get( 'status', 'done' )
 234        return trans.fill_template( '/webapps/community/index.mako', message=message, status=status )
 235    @web.expose
 236    def browse_categories( self, trans, **kwd ):
 237        if 'f-free-text-search' in kwd:
 238            # Trick to enable searching repository name, description from the CategoryListGrid.
 239            # What we've done is rendered the search box for the RepositoryListGrid on the grid.mako
 240            # template for the CategoryListGrid.  See ~/templates/webapps/community/category/grid.mako.
 241            # Since we are searching repositories and not categories, redirect to browse_repositories().
 242            if 'id' in kwd and 'f-free-text-search' in kwd and kwd[ 'id' ] == kwd[ 'f-free-text-search' ]:
 243                # The value of 'id' has been set to the search string, which is a repository name.
 244                # We'll try to get the desired encoded repository id to pass on.
 245                try:
 246                    repository = get_repository_by_name( trans, kwd[ 'id' ] )
 247                    kwd[ 'id' ] = trans.security.encode_id( repository.id )
 248                except:
 249                    pass
 250            return self.browse_repositories( trans, **kwd )
 251        if 'operation' in kwd:
 252            operation = kwd['operation'].lower()
 253            if operation in [ "repositories_by_category", "repositories_by_user" ]:
 254                # Eliminate the current filters if any exist.
 255                for k, v in kwd.items():
 256                    if k.startswith( 'f-' ):
 257                        del kwd[ k ]
 258                return trans.response.send_redirect( web.url_for( controller='repository',
 259                                                                  action='browse_repositories',
 260                                                                  **kwd ) )
 261        # Render the list view
 262        return self.category_list_grid( trans, **kwd )
 263    @web.expose
 264    def browse_downloadable_repositories( self, trans, **kwd ):
 265        # Set the toolshedgalaxyurl cookie so we can get back
 266        # to the calling local Galaxy instance.
 267        galaxy_url = kwd.get( 'galaxy_url', None )
 268        if galaxy_url:
 269            trans.set_cookie( galaxy_url, name='toolshedgalaxyurl' )
 270        repository_id = kwd.get( 'id', None )
 271        if 'operation' in kwd:
 272            operation = kwd[ 'operation' ].lower()
 273            if operation == "preview_tools_in_changeset":
 274                repository = get_repository( trans, repository_id )
 275                return trans.response.send_redirect( web.url_for( controller='repository',
 276                                                                  action='preview_tools_in_changeset',
 277                                                                  repository_id=repository_id,
 278                                                                  changeset_revision=repository.tip ) )
 279
 280        # The changeset_revision_select_field in the RepositoryListGrid performs a refresh_on_change
 281        # which sends in request parameters like changeset_revison_1, changeset_revision_2, etc.  One
 282        # of the many select fields on the grid performed the refresh_on_change, so we loop through 
 283        # all of the received values to see which value is not the repository tip.  If we find it, we
 284        # know the refresh_on_change occurred, and we have the necessary repository id and change set
 285        # revision to pass on.
 286        for k, v in kwd.items():
 287            changset_revision_str = 'changeset_revision_'
 288            if k.startswith( changset_revision_str ):
 289                repository_id = trans.security.encode_id( int( k.lstrip( changset_revision_str ) ) )
 290                repository = get_repository( trans, repository_id )
 291                if repository.tip != v:
 292                    return trans.response.send_redirect( web.url_for( controller='repository',
 293                                                                      action='preview_tools_in_changeset',
 294                                                                      repository_id=trans.security.encode_id( repository.id ),
 295                                                                      changeset_revision=v ) )
 296        url_args = dict( action='browse_downloadable_repositories',
 297                         operation='preview_tools_in_changeset',
 298                         repository_id=repository_id )
 299        self.downloadable_repository_list_grid.operations = [ grids.GridOperation( "Preview and install tools",
 300                                                                                   url_args=url_args,
 301                                                                                   allow_multiple=False,
 302                                                                                   async_compatible=False ) ]
 303
 304        # Render the list view
 305        return self.downloadable_repository_list_grid( trans, **kwd )
 306    @web.expose
 307    def preview_tools_in_changeset( self, trans, repository_id, **kwd ):
 308        params = util.Params( kwd )
 309        message = util.restore_text( params.get( 'message', '' ) )
 310        status = params.get( 'status', 'done' )
 311        repository = get_repository( trans, repository_id )
 312        changeset_revision = util.restore_text( params.get( 'changeset_revision', repository.tip ) )
 313        repository_metadata = get_repository_metadata_by_changeset_revision( trans, repository_id, changeset_revision )
 314        if repository_metadata:
 315            metadata = repository_metadata.metadata
 316        else:
 317            metadata = None
 318        revision_label = get_revision_label( trans, repository, changeset_revision )
 319        changeset_revision_select_field = build_changeset_revision_select_field( trans,
 320                                                                                 repository,
 321                                                                                 selected_value=changeset_revision,
 322                                                                                 add_id_to_name=False )
 323        return trans.fill_template( '/webapps/community/repository/preview_tools_in_changeset.mako',
 324                                    repository=repository,
 325                                    changeset_revision=changeset_revision,
 326                                    revision_label=revision_label,
 327                                    changeset_revision_select_field=changeset_revision_select_field,
 328                                    metadata=metadata,
 329                                    display_for_install=True,
 330                                    message=message,
 331                                    status=status )
 332    @web.expose
 333    def install_repository_revision( self, trans, repository_id, **kwd ):
 334        params = util.Params( kwd )
 335        message = util.restore_text( params.get( 'message', ''  ) )
 336        status = params.get( 'status', 'done' )
 337        galaxy_url = trans.get_cookie( name='toolshedgalaxyurl' )
 338        repository = get_repository( trans, repository_id )
 339        changeset_revision = util.restore_text( params.get( 'changeset_revision', repository.tip ) )
 340        # Redirect back to local Galaxy to perform install.
 341        tool_shed_url = trans.request.host
 342        repository_clone_url = generate_clone_url( trans, repository_id )
 343        # TODO: support https in the following url.
 344        url = 'http://%s/admin/install_tool_shed_repository?tool_shed_url=%s&repository_name=%s&repository_clone_url=%s&changeset_revision=%s' % \
 345            ( galaxy_url, tool_shed_url, repository.name, repository_clone_url, changeset_revision )
 346        return trans.response.send_redirect( url )
 347    @web.expose
 348    def browse_repositories( self, trans, **kwd ):
 349        # We add params to the keyword dict in this method in order to rename the param
 350        # with an "f-" prefix, simulating filtering by clicking a search link.  We have
 351        # to take this approach because the "-" character is illegal in HTTP requests.
 352        if 'operation' in kwd:
 353            operation = kwd['operation'].lower()
 354            if operation == "view_or_manage_repository":
 355                repository_id = kwd[ 'id' ]
 356                repository = get_repository( trans, repository_id )
 357                is_admin = trans.user_is_admin()
 358                if is_admin or repository.user == trans.user:
 359                    return trans.response.send_redirect( web.url_for( controller='repository',
 360                                                                      action='manage_repository',
 361                                                                      **kwd ) )
 362                else:
 363                    return trans.response.send_redirect( web.url_for( controller='repository',
 364                                                                      action='view_repository',
 365                                                                      **kwd ) )
 366            elif operation == "edit_repository":
 367                return trans.response.send_redirect( web.url_for( controller='repository',
 368                                                                  action='edit_repository',
 369                                                                  **kwd ) )
 370            elif operation == "repositories_by_user":
 371                # Eliminate the current filters if any exist.
 372                for k, v in kwd.items():
 373                    if k.startswith( 'f-' ):
 374                        del kwd[ k ]
 375                if 'user_id' in kwd:
 376                    user = get_user( trans, kwd[ 'user_id' ] )
 377                    kwd[ 'f-email' ] = user.email
 378                    del kwd[ 'user_id' ]
 379                else:
 380                    # The received id is the repository id, so we need to get the id of the user
 381                    # that uploaded the repository.
 382                    repository_id = kwd.get( 'id', None )
 383                    repository = get_repository( trans, repository_id )
 384                    kwd[ 'f-email' ] = repository.user.email
 385            elif operation == "my_repositories":
 386                # Eliminate the current filters if any exist.
 387                for k, v in kwd.items():
 388                    if k.startswith( 'f-' ):
 389                        del kwd[ k ]
 390                kwd[ 'f-email' ] = trans.user.email
 391            elif operation == "repositories_by_category":
 392                # Eliminate the current filters if any exist.
 393                for k, v in kwd.items():
 394                    if k.startswith( 'f-' ):
 395                        del kwd[ k ]
 396                category_id = kwd.get( 'id', None )
 397                category = get_category( trans, category_id )
 398                kwd[ 'f-Category.name' ] = category.name
 399            elif operation == "receive email alerts":
 400                if trans.user:
 401                    if kwd[ 'id' ]:
 402                        return trans.response.send_redirect( web.url_for( controller='repository',
 403                                                                          action='set_email_alerts',
 404                                                                          **kwd ) )
 405                else:
 406                    kwd[ 'message' ] = 'You must be logged in to set email alerts.'
 407                    kwd[ 'status' ] = 'error'
 408                    del kwd[ 'operation' ]
 409        # The changeset_revision_select_field in the RepositoryListGrid performs a refresh_on_change
 410        # which sends in request parameters like changeset_revison_1, changeset_revision_2, etc.  One
 411        # of the many select fields on the grid performed the refresh_on_change, so we loop through 
 412        # all of the received values to see which value is not the repository tip.  If we find it, we
 413        # know the refresh_on_change occurred, and we have the necessary repository id and change set
 414        # revision to pass on.
 415        for k, v in kwd.items():
 416            changset_revision_str = 'changeset_revision_'
 417            if k.startswith( changset_revision_str ):
 418                repository_id = trans.security.encode_id( int( k.lstrip( changset_revision_str ) ) )
 419                repository = get_repository( trans, repository_id )
 420                if repository.tip != v:
 421                    return trans.response.send_redirect( web.url_for( controller='repository',
 422                                                                      action='browse_repositories',
 423                                                                      operation='view_or_manage_repository',
 424                                                                      id=trans.security.encode_id( repository.id ),
 425                                                                      changeset_revision=v ) )
 426        # Render the list view
 427        return self.repository_list_grid( trans, **kwd )
 428    @web.expose
 429    def create_repository( self, trans, **kwd ):
 430        params = util.Params( kwd )
 431        message = util.restore_text( params.get( 'message', ''  ) )
 432        status = params.get( 'status', 'done' )
 433        categories = get_categories( trans )
 434        if not categories:
 435            message = 'No categories have been configured in this instance of the Galaxy Tool Shed.  ' + \
 436                'An administrator needs to create some via the Administrator control panel before creating repositories.',
 437            status = 'error'
 438            return trans.response.send_redirect( web.url_for( controller='repository',
 439                                                              action='browse_repositories',
 440                                                              message=message,
 441                                                              status=status ) )
 442        name = util.restore_text( params.get( 'name', '' ) )
 443        description = util.restore_text( params.get( 'description', '' ) )
 444        long_description = util.restore_text( params.get( 'long_description', '' ) )
 445        category_ids = util.listify( params.get( 'category_id', '' ) )
 446        selected_categories = [ trans.security.decode_id( id ) for id in category_ids ]
 447        if params.get( 'create_repository_button', False ):
 448            error = False
 449            message = self.__validate_repository_name( name, trans.user )
 450            if message:
 451                error = True
 452            if not description:
 453                message = 'Enter a description.'
 454                error = True
 455            if not error:
 456                # Add the repository record to the db
 457                repository = trans.app.model.Repository( name=name,
 458                                                         description=description,
 459                                                         long_description=long_description,
 460                                                         user_id=trans.user.id )
 461                # Flush to get the id
 462                trans.sa_session.add( repository )
 463                trans.sa_session.flush()
 464                # Determine the repository's repo_path on disk
 465                dir = os.path.join( trans.app.config.file_path, *directory_hash_id( repository.id ) )
 466                # Create directory if it does not exist
 467                if not os.path.exists( dir ):
 468                    os.makedirs( dir )
 469                # Define repo name inside hashed directory
 470                repository_path = os.path.join( dir, "repo_%d" % repository.id )
 471                # Create local repository directory
 472                if not os.path.exists( repository_path ):
 473                    os.makedirs( repository_path )
 474                # Create the local repository
 475                repo = hg.repository( get_configured_ui(), repository_path, create=True )
 476                # Add an entry in the hgweb.config file for the local repository
 477                # This enables calls to repository.repo_path
 478                self.__add_hgweb_config_entry( trans, repository, repository_path )
 479                # Create a .hg/hgrc file for the local repository
 480                self.__create_hgrc_file( repository )
 481                flush_needed = False
 482                if category_ids:
 483                    # Create category associations
 484                    for category_id in category_ids:
 485                        category = trans.app.model.Category.get( trans.security.decode_id( category_id ) )
 486                        rca = trans.app.model.RepositoryCategoryAssociation( repository, category )
 487                        trans.sa_session.add( rca )
 488                        flush_needed = True
 489                if flush_needed:
 490                    trans.sa_session.flush()
 491                message = "Repository '%s' has been created." % repository.name
 492                trans.response.send_redirect( web.url_for( controller='repository',
 493                                                           action='view_repository',
 494                                                           message=message,
 495                                                           id=trans.security.encode_id( repository.id ) ) )
 496        return trans.fill_template( '/webapps/community/repository/create_repository.mako',
 497                                    name=name,
 498                                    description=description,
 499                                    long_description=long_description,
 500                                    selected_categories=selected_categories,
 501                                    categories=categories,
 502                                    message=message,
 503                                    status=status )
 504    def __validate_repository_name( self, name, user ):
 505        # Repository names must be unique for each user, must be at least four characters
 506        # in length and must contain only lower-case letters, numbers, and the '_' character.
 507        if name in [ 'None', None, '' ]:
 508            return 'Enter the required repository name.'
 509        for repository in user.active_repositories:
 510            if repository.name == name:
 511                return "You already have a repository named '%s', so choose a different name." % name
 512        if len( name ) < 4:
 513            return "Repository names must be at least 4 characters in length."
 514        if len( name ) > 80:
 515            return "Repository names cannot be more than 80 characters in length."
 516        if not( VALID_REPOSITORYNAME_RE.match( name ) ):
 517            return "Repository names must contain only lower-case letters, numbers and underscore '_'."
 518        return ''
 519    def __make_hgweb_config_copy( self, trans, hgweb_config ):
 520        # Make a backup of the hgweb.config file
 521        today = date.today()
 522        backup_date = today.strftime( "%Y_%m_%d" )
 523        hgweb_config_copy = '%s/hgweb.config_%s_backup' % ( trans.app.config.root, backup_date )
 524        shutil.copy( os.path.abspath( hgweb_config ), os.path.abspath( hgweb_config_copy ) )
 525    def __add_hgweb_config_entry( self, trans, repository, repository_path ):
 526        # Add an entry in the hgweb.config file for a new repository.
 527        # An entry looks something like:
 528        # repos/test/mira_assembler = database/community_files/000/repo_123.
 529        hgweb_config = "%s/hgweb.config" %  trans.app.config.root
 530        # Make a backup of the hgweb.config file since we're going to be changing it.
 531        self.__make_hgweb_config_copy( trans, hgweb_config )
 532        entry = "repos/%s/%s = %s" % ( repository.user.username, repository.name, repository_path.lstrip( './' ) )
 533        if os.path.exists( hgweb_config ):
 534            output = open( hgweb_config, 'a' )
 535        else:
 536            output = open( hgweb_config, 'w' )
 537            output.write( '[paths]\n' )
 538        output.write( "%s\n" % entry )
 539        output.close()
 540    def __change_hgweb_config_entry( self, trans, repository, old_repository_name, new_repository_name ):
 541        # Change an entry in the hgweb.config file for a repository.  This only happens when
 542        # the owner changes the name of the repository.  An entry looks something like:
 543        # repos/test/mira_assembler = database/community_files/000/repo_123.
 544        hgweb_config = "%s/hgweb.config" % trans.app.config.root
 545        # Make a backup of the hgweb.config file since we're going to be changing it.
 546        self.__make_hgweb_config_copy( trans, hgweb_config )
 547        repo_dir = repository.repo_path
 548        old_lhs = "repos/%s/%s" % ( repository.user.username, old_repository_name )
 549        old_entry = "%s = %s" % ( old_lhs, repo_dir )
 550        new_entry = "repos/%s/%s = %s\n" % ( repository.user.username, new_repository_name, repo_dir )
 551        tmp_fd, tmp_fname = tempfile.mkstemp()
 552        new_hgweb_config = open( tmp_fname, 'wb' )
 553        for i, line in enumerate( open( hgweb_config ) ):
 554            if line.startswith( old_lhs ):
 555                new_hgweb_config.write( new_entry )
 556            else:
 557                new_hgweb_config.write( line )
 558        shutil.move( tmp_fname, os.path.abspath( hgweb_config ) )
 559    def __create_hgrc_file( self, repository ):
 560        # At this point, an entry for the repository is required to be in the hgweb.config
 561        # file so we can call repository.repo_path.
 562        # Create a .hg/hgrc file that looks something like this:
 563        # [web]
 564        # allow_push = test
 565        # name = convert_characters1
 566        # push_ssl = False
 567        # Since we support both http and https, we set push_ssl to False to override
 568        # the default (which is True) in the mercurial api.
 569        repo = hg.repository( get_configured_ui(), path=repository.repo_path )
 570        fp = repo.opener( 'hgrc', 'wb' )
 571        fp.write( '[paths]\n' )
 572        fp.write( 'default = .\n' )
 573        fp.write( 'default-push = .\n' )
 574        fp.write( '[web]\n' )
 575        fp.write( 'allow_push = %s\n' % repository.user.username )
 576        fp.write( 'name = %s\n' % repository.name )
 577        fp.write( 'push_ssl = false\n' )
 578        fp.close()
 579    @web.expose
 580    def browse_repository( self, trans, id, **kwd ):
 581        params = util.Params( kwd )
 582        message = util.restore_text( params.get( 'message', ''  ) )
 583        status = params.get( 'status', 'done' )
 584        commit_message = util.restore_text( params.get( 'commit_message', 'Deleted selected files' ) )
 585        repository = get_repository( trans, id )
 586        repo = hg.repository( get_configured_ui(), repository.repo_path )
 587        current_working_dir = os.getcwd()
 588        # Update repository files for browsing.
 589        update_for_browsing( trans, repository, current_working_dir, commit_message=commit_message )
 590        is_malicious = change_set_is_malicious( trans, id, repository.tip )
 591        return trans.fill_template( '/webapps/community/repository/browse_repository.mako',
 592                                    repo=repo,
 593                                    repository=repository,
 594                                    commit_message=commit_message,
 595                                    is_malicious=is_malicious,
 596                                    message=message,
 597                                    status=status )
 598    @web.expose
 599    def contact_owner( self, trans, id, **kwd ):
 600        params = util.Params( kwd )
 601        message = util.restore_text( params.get( 'message', ''  ) )
 602        status = params.get( 'status', 'done' )
 603        repository = get_repository( trans, id )
 604        if trans.user and trans.user.email:
 605            return trans.fill_template( "/webapps/community/repository/contact_owner.mako",
 606                                        repository=repository,
 607                                        message=message,
 608                                        status=status )
 609        else:
 610            # Do all we can to eliminate spam.
 611            return trans.show_error_message( "You must be logged in to contact the owner of a repository." )
 612    @web.expose
 613    def send_to_owner( self, trans, id, message='' ):
 614        repository = get_repository( trans, id )
 615        if not message:
 616            message = 'Enter a message'
 617            status = 'error'
 618        elif trans.user and trans.user.email:
 619            smtp_server = trans.app.config.smtp_server
 620            from_address = trans.app.config.email_from
 621            if smtp_server is None or from_address is None:
 622                return trans.show_error_message( "Mail is not configured for this Galaxy tool shed instance" )
 623            to_address = repository.user.email
 624            # Get the name of the server hosting the tool shed instance.
 625            host = trans.request.host
 626            # Build the email message
 627            body = string.Template( contact_owner_template ) \
 628                .safe_substitute( username=trans.user.username,
 629                                  repository_name=repository.name,
 630                                  email=trans.user.email,
 631                                  message=message,
 632                                  host=host )
 633            subject = "Regarding your tool shed repository named %s" % repository.name
 634            # Send it
 635            try:
 636                util.send_mail( from_address, to_address, subject, body, trans.app.config )
 637                message = "Your message has been sent"
 638                status = "done"
 639            except Exception, e:
 640                message = "An error occurred sending your message by email: %s" % str( e )
 641                status = "error"
 642        else:
 643            # Do all we can to eliminate spam.
 644            return trans.show_error_message( "You must be logged in to contact the owner of a repository." )
 645        return trans.response.send_redirect( web.url_for( controller='repository',
 646                                                          action='contact_owner',
 647                                                          id=id,
 648                                                          message=message,
 649                                                          status=status ) )
 650    @web.expose
 651    def select_files_to_delete( self, trans, id, **kwd ):
 652        params = util.Params( kwd )
 653        message = util.restore_text( params.get( 'message', '' ) )
 654        status = params.get( 'status', 'done' )
 655        commit_message = util.restore_text( params.get( 'commit_message', 'Deleted selected files' ) )
 656        repository = get_repository( trans, id )
 657        repo_dir = repository.repo_path
 658        repo = hg.repository( get_configured_ui(), repo_dir )
 659        selected_files_to_delete = util.restore_text( params.get( 'selected_files_to_delete', '' ) )
 660        if params.get( 'select_files_to_delete_button', False ):
 661            if selected_files_to_delete:
 662                selected_files_to_delete = selected_files_to_delete.split( ',' )
 663                current_working_dir = os.getcwd()
 664                # Get the current repository tip.
 665                tip = repository.tip
 666                for selected_file in selected_files_to_delete:
 667                    try:
 668                        commands.remove( repo.ui, repo, repo_file, force=True )
 669                    except Exception, e:
 670                        # I never have a problem with commands.remove on a Mac, but in the test/production
 671                        # tool shed environment, it throws an exception whenever I delete all files from a
 672                        # repository.  If this happens, we'll try the following.
 673                        relative_selected_file = selected_file.split( 'repo_%d' % repository.id )[1].lstrip( '/' )
 674                        repo.dirstate.remove( relative_selected_file )
 675                        repo.dirstate.write()
 676                        absolute_selected_file = os.path.abspath( selected_file )
 677                        if os.path.isdir( absolute_selected_file ):
 678                            try:
 679                                os.rmdir( absolute_selected_file )
 680                            except OSError, e:
 681                                # The directory is not empty
 682                                pass
 683                        elif os.path.isfile( absolute_selected_file ):
 684                            os.remove( absolute_selected_file )
 685                            dir = os.path.split( absolute_selected_file )[0]
 686                            try:
 687                                os.rmdir( dir )
 688                            except OSError, e:
 689                                # The directory is not empty
 690                                pass
 691                # Commit the change set.
 692                if not commit_message:
 693                    commit_message = 'Deleted selected files'
 694                try:
 695                    commands.commit( repo.ui, repo, repo_dir, user=trans.user.username, message=commit_message )
 696                except Exception, e:
 697                    # I never have a problem with commands.commit on a Mac, but in the test/production
 698                    # tool shed environment, it occasionally throws a "TypeError: array item must be char"
 699                    # exception.  If this happens, we'll try the following.
 700                    repo.dirstate.write()
 701                    repo.commit( user=trans.user.username, text=commit_message )
 702                handle_email_alerts( trans, repository )
 703                # Update the repository files for browsing.
 704                update_for_browsing( trans, repository, current_working_dir, commit_message=commit_message )
 705                # Get the new repository tip.
 706                repo = hg.repository( get_configured_ui(), repo_dir )
 707                if tip != repository.tip:
 708                    message = "The selected files were deleted from the repository."
 709                else:
 710                    message = 'No changes to repository.'
 711                # Set metadata on the repository tip
 712                error_message, status = set_repository_metadata( trans, id, repository.tip, **kwd )
 713                if error_message:
 714                    message = '%s<br/>%s' % ( message, error_message )
 715                    return trans.response.send_redirect( web.url_for( controller='repository',
 716                                                                      action='manage_repository',
 717                                                                      id=id,
 718                                                                      message=message,
 719                                                                      status=status ) )
 720            else:
 721                message = "Select at least 1 file to delete from the repository before clicking <b>Delete selected files</b>."
 722                status = "error"
 723        is_malicious = change_set_is_malicious( trans, id, repository.tip )
 724        return trans.fill_template( '/webapps/community/repository/browse_repository.mako',
 725                                    repo=repo,
 726                                    repository=repository,
 727                                    commit_message=commit_message,
 728                                    is_malicious=is_malicious,
 729                                    message=message,
 730                                    status=status )
 731    @web.expose
 732    def view_repository( self, trans, id, **kwd ):
 733        params = util.Params( kwd )
 734        message = util.restore_text( params.get( 'message', ''  ) )
 735        status = params.get( 'status', 'done' )
 736        repository = get_repository( trans, id )
 737        repo = hg.repository( get_configured_ui(), repository.repo_path )
 738        avg_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, repository, webapp_model=trans.model )
 739        changeset_revision = util.restore_text( params.get( 'changeset_revision', repository.tip ) )
 740        display_reviews = util.string_as_bool( params.get( 'display_reviews', False ) )
 741        alerts = params.get( 'alerts', '' )
 742        alerts_checked = CheckboxField.is_checked( alerts )
 743        if repository.email_alerts:
 744            email_alerts = from_json_string( repository.email_alerts )
 745        else:
 746            email_alerts = []
 747        user = trans.user
 748        if user and params.get( 'receive_email_alerts_button', False ):
 749            flush_needed = False
 750            if alerts_checked:
 751                if user.email not in email_alerts:
 752                    email_alerts.append( user.email )
 753                    repository.email_alerts = to_json_string( email_alerts )
 754                    flush_needed = True
 755            else:
 756                if user.email in email_alerts:
 757                    email_alerts.remove( user.email )
 758                    repository.email_alerts = to_json_string( email_alerts )
 759                    flush_needed = True
 760            if flush_needed:
 761                trans.sa_session.add( repository )
 762                trans.sa_session.flush()
 763        checked = alerts_checked or ( user and user.email in email_alerts )
 764        alerts_check_box = CheckboxField( 'alerts', checked=checked )
 765        changeset_revision_select_field = build_changeset_revision_select_field( trans,
 766                                                                                 repository,
 767                                                                                 selected_value=changeset_revision,
 768                                                                                 add_id_to_name=False )
 769        revision_label = get_revision_label( trans, repository, changeset_revision )
 770        repository_metadata = get_repository_metadata_by_changeset_revision( trans, id, changeset_revision )
 771        if repository_metadata:
 772            metadata = repository_metadata.metadata
 773        else:
 774            metadata = None
 775        is_malicious = change_set_is_malicious( trans, id, repository.tip )
 776        if is_malicious:
 777            if trans.app.security_agent.can_push( trans.user, repository ):
 778                message += malicious_error_can_push
 779            else:
 780                message += malicious_error
 781            status = 'error'
 782        return trans.fill_template( '/webapps/community/repository/view_repository.mako',
 783                                    repo=repo,
 784                                    repository=repository,
 785                                    metadata=metadata,
 786                                    avg_rating=avg_rating,
 787                                    display_reviews=display_reviews,
 788                                    num_ratings=num_ratings,
 789                                    alerts_check_box=alerts_check_box,
 790                                    changeset_revision=changeset_revision,
 791                                    changeset_revision_select_field=changeset_revision_select_field,
 792                                    revision_label=revision_label,
 793                                    is_malicious=is_malicious,
 794                                    message=message,
 795                                    status=status )
 796    @web.expose
 797    @web.require_login( "manage repository" )
 798    def manage_repository( self, trans, id, **kwd ):
 799        params = util.Params( kwd )
 800        message = util.restore_text( params.get( 'message', ''  ) )
 801        status = params.get( 'status', 'done' )
 802        repository = get_repository( trans, id )
 803        repo_dir = repository.repo_path
 804        repo = hg.repository( get_configured_ui(), repo_dir )
 805        repo_name = util.restore_text( params.get( 'repo_name', repository.name ) )
 806        changeset_revision = util.restore_text( params.get( 'changeset_revision', repository.tip ) )
 807        description = util.restore_text( params.get( 'description', repository.description ) )
 808        long_description = util.restore_text( params.get( 'long_description', repository.long_description ) )
 809        avg_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, repository, webapp_model=trans.model )
 810        display_reviews = util.string_as_bool( params.get( 'display_reviews', False ) )
 811        alerts = params.get( 'alerts', '' )
 812        alerts_checked = CheckboxField.is_checked( alerts )
 813        category_ids = util.listify( params.get( 'category_id', '' ) )
 814        if repository.email_alerts:
 815            email_alerts = from_json_string( repository.email_alerts )
 816        else:
 817            email_alerts = []
 818        allow_push = params.get( 'allow_push', '' )
 819        error = False
 820        user = trans.user
 821        if params.get( 'edit_repository_button', False ):
 822            flush_needed = False
 823            # TODO: add a can_manage in the security agent.
 824            if user != repository.user:
 825                message = "You are not the owner of this repository, so you cannot manage it."
 826                status = error
 827                return trans.response.send_redirect( web.url_for( controller='repository',
 828                                                                  action='view_repository',
 829                                                                  id=id,
 830                                                                  message=message,
 831                                                                  status=status ) )
 832            if repo_name != repository.name:
 833                message = self.__validate_repository_name( repo_name, user )
 834                if message:
 835                    error = True
 836                else:
 837                    self.__change_hgweb_config_entry( trans, repository, repository.name, repo_name )
 838                    repository.name = repo_name
 839                    flush_needed = True
 840            if description != repository.description:
 841                repository.description = description
 842                flush_needed = True
 843            if long_description != repository.long_description:
 844                repository.long_description = long_description
 845                flush_needed = True
 846            if flush_needed:
 847                trans.sa_session.add( repository )
 848                trans.sa_session.flush()
 849            message = "The repository information has been updated."
 850        elif params.get( 'manage_categories_button', False ):
 851            flush_needed = False
 852            # Delete all currently existing categories.
 853            for rca in repository.categories:
 854                trans.sa_session.delete( rca )
 855                trans.sa_session.flush()
 856            if category_ids:
 857                # Create category associations
 858                for category_id in category_ids:
 859                    category = trans.app.model.Category.get( trans.security.decode_id( category_id ) )
 860                    rca = trans.app.model.RepositoryCategoryAssociation( repository, category )
 861                    trans.sa_session.add( rca )
 862                    trans.sa_session.flush()
 863            message = "The repository information has been updated."
 864        elif params.get( 'user_access_button', False ):
 865            if allow_push not in [ 'none' ]:
 866                remove_auth = params.get( 'remove_auth', '' )
 867                if remove_auth:
 868                    usernames = ''
 869                else:
 870                    user_ids = util.listify( allow_push )
 871                    usernames = []
 872                    for user_id in user_ids:
 873                        user = trans.sa_session.query( trans.model.User ).get( trans.security.decode_id( user_id ) )
 874                        usernames.append( user.username )
 875                    usernames = ','.join( usernames )
 876                repository.set_allow_push( usernames, remove_auth=remove_auth )
 877            message = "The repository information has been updated."
 878        elif params.get( 'receive_email_alerts_button', False ):
 879            flush_needed = False
 880            if alerts_checked:
 881                if user.email not in email_alerts:
 882                    email_alerts.append( user.email )
 883                    repository.email_alerts = to_json_string( email_alerts )
 884                    flush_needed = True
 885            else:
 886                if user.email in email_alerts:
 887                    email_alerts.remove( user.email )
 888                    repository.email_alerts = to_json_string( email_alerts )
 889                    flush_needed = True
 890            if flush_needed:
 891                trans.sa_session.add( repository )
 892                trans.sa_session.flush()
 893            message = "The repository information has been updated."
 894        if error:
 895            status = 'error'
 896        if repository.allow_push:
 897            current_allow_push_list = repository.allow_push.split( ',' )
 898        else:
 899            current_allow_push_list = []
 900        allow_push_select_field = self.__build_allow_push_select_field( trans, current_allow_push_list )
 901        checked = alerts_checked or user.email in email_alerts
 902        alerts_check_box = CheckboxField( 'alerts', checked=checked )
 903        changeset_revision_select_field = build_changeset_revision_select_field( trans,
 904                                                                                 repository,
 905                                                                                 selected_value=changeset_revision,
 906                                                                                 add_id_to_name=False )
 907        revision_label = get_revision_label( trans, repository, changeset_revision )
 908        repository_metadata = get_repository_metadata_by_changeset_revision( trans, id, changeset_revision )
 909        if repository_metadata:
 910            metadata = repository_metadata.metadata
 911            is_malicious = repository_metadata.malicious
 912        else:
 913            metadata = None
 914            is_malicious = False
 915        if is_malicious:
 916            if trans.app.security_agent.can_push( trans.user, repository ):
 917                message += malicious_error_can_push
 918            else:
 919                message += malicious_error
 920            status = 'error'
 921        malicious_check_box = CheckboxField( 'malicious', checked=is_malicious )
 922        categories = get_categories( trans )
 923        selected_categories = [ rca.category_id for rca in repository.categories ]
 924        return trans.fill_template( '/webapps/community/repository/manage_repository.mako',
 925                                    repo_name=repo_name,
 926                                    description=description,
 927                                    long_description=long_description,
 928                                    current_allow_push_list=current_allow_push_list,
 929                                    allow_push_select_field=allow_push_select_field,
 930                                    repo=repo,
 931                                    repository=repository,
 932                                    changeset_revision=changeset_revision,
 933                                    changeset_revision_select_field=changeset_revision_select_field,
 934                                    revision_label=revision_label,
 935                                    selected_categories=selected_categories,
 936                                    categories=categories,
 937                                    metadata=metadata,
 938                                    avg_rating=avg_rating,
 939                                    display_reviews=display_reviews,
 940                                    num_ratings=num_ratings,
 941                                    alerts_check_box=alerts_check_box,
 942                                    malicious_check_box=malicious_check_box,
 943                                    is_malicious=is_malicious,
 944                                    message=message,
 945                                    status=status )
 946    @web.expose
 947    def view_changelog( self, trans, id, **kwd ):
 948        params = util.Params( kwd )
 949        message = util.restore_text( params.get( 'message', ''  ) )
 950        status = params.get( 'status', 'done' )
 951        repository = get_repository( trans, id )
 952        repo = hg.repository( get_configured_ui(), repository.repo_path )
 953        changesets = []
 954        for changeset in repo.changelog:
 955            ctx = repo.changectx( changeset )
 956            t, tz = ctx.date()
 957            date = datetime( *time.gmtime( float( t ) - tz )[:6] )
 958            display_date = date.strftime( "%Y-%m-%d" )
 959            change_dict = { 'ctx' : ctx,
 960                            'rev' : str( ctx.rev() ),
 961                            'date' : date,
 962                            'display_date' : display_date,
 963                            'description' : ctx.description(),
 964                            'files' : ctx.files(),
 965                            'user' : ctx.user(),
 966                            'parent' : ctx.parents()[0] }
 967            # Make sure we'll view latest changeset first.
 968            changesets.insert( 0, change_dict )
 969        is_malicious = change_set_is_malicious( trans, id, repository.tip )
 970        return trans.fill_template( '/webapps/community/repository/view_changelog.mako', 
 971                                    repository=repository,
 972                                    changesets=changesets,
 973                                    is_malicious=is_malicious,
 974                                    message=message,
 975                                    status=status )
 976    @web.expose
 977    def view_changeset( self, trans, id, ctx_str, **kwd ):
 978        params = util.Params( kwd )
 979        message = util.restore_text( params.get( 'message', ''  ) )
 980        status = params.get( 'status', 'done' )
 981        repository = get_repository( trans, id )
 982        repo = hg.repository( get_configured_ui(), repository.repo_path )
 983        ctx = get_changectx_for_changeset( trans, repo, ctx_str )
 984        if ctx is None:
 985            message = "Repository does not include changeset revision '%s'." % str( ctx_str )
 986            status = 'error'
 987            return trans.response.send_redirect( web.url_for( controller='repository',
 988                                                              action='view_changelog',
 989                                                              id=id,
 990                                                              message=message,
 991                                                              status=status ) )
 992        ctx_parent = ctx.parents()[0]
 993        modified, added, removed, deleted, unknown, ignored, clean = repo.status( node1=ctx_parent.node(), node2=ctx.node() )
 994        anchors = modified + added + removed + deleted + unknown + ignored + clean
 995        diffs = []
 996        for diff in patch.diff( repo, node1=ctx_parent.node(), node2=ctx.node() ):
 997            diffs.append( self.to_html_escaped( diff ) )
 998        is_malicious = change_set_is_malicious( trans, id, repository.tip )
 999        return trans.fill_template( '/webapps/community/repository/view_changeset.mako', 
1000                                    repository=repository,
1001                                    ctx=ctx,
1002                                    anchors=anchors,
1003                                    modified=modified,
1004                                    added=added,
1005                                    removed=removed,
1006                                    deleted=deleted,
1007                                    unknown=unknown,
1008                                    ignored=ignored,
1009                                    clean=clean,
1010                                    diffs=diffs,
1011                                    is_malicious=is_malicious,
1012                                    message=message,
1013                                    status=status )
1014    @web.expose
1015    @web.require_login( "rate repositories" )
1016    def rate_repository( self, trans, **kwd ):
1017        """ Rate a repository and return updated rating data. """
1018        params = util.Params( kwd )
1019        message = util.restore_text( params.get( 'message', ''  ) )
1020        status = params.get( 'status', 'done' )
1021        id = params.get( 'id', None )
1022        if not id:
1023            return trans.response.send_redirect( web.url_for( controller='repository',
1024                                                              action='browse_repositories',
1025                                                              message='Select a repository to rate',
1026                                                              status='error' ) )
1027        repository = get_repository( trans, id )
1028        repo = hg.repository( get_configured_ui(), repository.repo_path )
1029        if repository.user == trans.user:
1030            return trans.response.send_redirect( web.url_for( controller='repository',
1031                                                              action='browse_repositories',
1032                                                              message="You are not allowed to rate your own repository",
1033                                                              status='error' ) )
1034        if params.get( 'rate_button', False ):
1035            rating = int( params.get( 'rating', '0' ) )
1036            comment = util.restore_text( params.get( 'comment', '' ) )
1037            rating = self.rate_item( trans, trans.user, repository, rating, comment )
1038        avg_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, repository, webapp_model=trans.model )
1039        display_reviews = util.string_as_bool( params.get( 'display_reviews', False ) )
1040        rra = self.get_user_item_rating( trans.sa_session, trans.user, repository, webapp_model=trans.model )
1041        is_malicious = change_set_is_malicious( trans, id, repository.tip )
1042        return trans.fill_template( '/webapps/community/repository/rate_repository.mako', 
1043                                    repository=repository,
1044                                    avg_rating=avg_rating,
1045                                    display_reviews=display_reviews,
1046                                    num_ratings=num_ratings,
1047                                    rra=rra,
1048                                    is_malicious=is_malicious,
1049                                    message=message,
1050                                    status=status )
1051    @web.expose
1052    @web.require_login( "set email alerts" )
1053    def set_email_alerts( self, trans, **kwd ):
1054        # Set email alerts for selected repositories
1055        params = util.Params( kwd )
1056        user = trans.user
1057        if user:
1058            repository_ids = util.listify( kwd.get( 'id', '' ) )
1059            total_alerts_added = 0
1060            total_alerts_removed = 0
1061            flush_needed = False
1062            for repository_id in repository_ids:
1063                repository = get_repository( trans, repository_id )
1064                if repository.email_alerts:
1065                    email_alerts = from_json_string( repository.email_alerts )
1066                else:
1067                    email_alerts = []
1068                if user.email in email_alerts:
1069                    email_alerts.remove( user.email )
1070                    repository.email_alerts = to_json_string( email_alerts )
1071                    trans.sa_session.add( repository )
1072                    flush_needed = True
1073                    total_alerts_removed += 1
1074                else:
1075                    email_alerts.append( user.email )
1076                    repository.email_alerts = to_json_string( email_alerts )
1077                    trans.sa_session.add( repository )
1078                    flush_needed = True
1079                    total_alerts_added += 1
1080            if flush_needed:
1081                trans.sa_session.flush()
1082            message = 'Total alerts added: %d, total alerts removed: %d' % ( total_alerts_added, total_alerts_removed )
1083            kwd[ 'message' ] = message
1084            kwd[ 'status' ] = 'done'
1085        del kwd[ 'operation' ]
1086        return trans.response.send_redirect( web.url_for( controller='repository',
1087                                                          action='browse_repositories',
1088                                                          **kwd ) )
1089    @web.expose
1090    @web.require_login( "set repository metadata" )
1091    def set_metadata( self, trans, id, ctx_str, **kwd ):
1092        malicious = kwd.get( 'malicious', '' )
1093        if kwd.get( 'malicious_button', False ):
1094            repository_metadata = get_repository_metadata_by_changeset_revision( trans, id, ctx_str )
1095            malicious_checked = CheckboxField.is_checked( malicious )
1096            repository_metadata.malicious = malicious_checked
1097            trans.sa_session.add( repository_metadata )
1098            trans.sa_session.flush()
1099            if malicious_checked:
1100                message = "The repository tip has been defined as malicious."
1101            else:
1102                message = "The repository tip has been defined as <b>not</b> malicious."
1103            status = 'done'
1104        else:
1105            # The set_metadata_button was clicked
1106            message, status = set_repository_metadata( trans, id, ctx_str, **kwd )
1107            if not message:
1108                message = "Metadata for change set revision '%s' has been reset." % str( ctx_str )
1109        return trans.response.send_redirect( web.url_for( controller='repository',
1110                                                          action='manage_repository',
1111                                                          id=id,
1112                                                          changeset_revision=ctx_str,
1113                                                          malicious=malicious,
1114                                                          message=message,
1115                                                          status=status ) )
1116    @web.expose
1117    def display_tool( self, trans, repository_id, tool_config, changeset_revision, **kwd ):
1118        params = util.Params( kwd )
1119        message = util.restore_text( params.get( 'message', ''  ) )
1120        status = params.get( 'status', 'done' )
1121        display_for_install = util.string_as_bool( params.get( 'display_for_install', False ) )
1122        repository = get_repository( trans, repository_id )
1123        repo = hg.repository( get_configured_ui(), repository.repo_path )          
1124        try:
1125            if changeset_revision == repository.tip:
1126                # Get the tool config from the file system we use for browsing.
1127                tool = load_tool( trans, os.path.abspath( tool_config ) )
1128            else:
1129                # Get the tool config file name from the hgweb url, something like:
1130                # /repos/test/convert_chars1/file/e58dcf0026c7/convert_characters.xml
1131                old_tool_config_file_name = tool_config.split( '/' )[ -1 ]
1132                ctx = get_changectx_for_changeset( trans, repo, changeset_revision )
1133                fctx = None
1134                for filename in ctx:
1135                    filename_head, filename_tail = os.path.split( filename )
1136                    if filename_tail == old_tool_config_file_name:
1137                        fctx = ctx[ filename ]
1138                        break
1139                if fctx:
1140                    # Write the contents of the old tool config to a temporary file.
1141                    fh = tempfile.NamedTemporaryFile( 'w' )
1142                    tmp_filename = fh.name
1143                    fh.close()
1144                    fh = open( tmp_fi