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

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 1144 lines · 1023 code · 11 blank · 110 comment · 180 complexity · aa433671131278cfec4260685f873f4f MD5 · raw file

Large files are truncated click here to view the full file

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