Source

galaxy-central / lib / galaxy / web / controllers / visualization.py

Greg Von Kuster d6fddb0 
jeremy goecks 9a5ecad 
James Taylor 811eda3 
jeremy goecks c3eccab 
James Taylor 811eda3 




jeremy goecks c3eccab 
James Taylor 811eda3 
Kanwei Li 432a32b 
jeremy goecks c3eccab 
James Taylor 811eda3 
jeremy goecks 5d3e57e 
James Taylor 811eda3 
jeremy goecks 5d3e57e 


James Taylor 811eda3 

jeremy goecks c3eccab 






jeremy goecks 0bbf30a 


James Taylor 811eda3 
Kanwei Li 2ac1139 
Kanwei Li 0890f7b 
jeremy goecks 8e9d2b4 
jeremy goecks c3eccab 

James Taylor 811eda3 
Greg Von Kuster d60f24c 
jeremy goecks c3eccab 







jeremy goecks eea6144 
jeremy goecks c3eccab 

jeremy goecks 5d3e57e 

jeremy goecks a8bfcfe 
jeremy goecks 1f3f263 
jeremy goecks 5d3e57e 
jeremy goecks eea6144 
jeremy goecks c3eccab 


jeremy goecks a8bfcfe 

jeremy goecks c3eccab 

Greg Von Kuster d60f24c 
jeremy goecks c3eccab 
Greg Von Kuster 7faa12a 
Greg Von Kuster d60f24c 
jeremy goecks c3eccab 
James Taylor 811eda3 
jeremy goecks c3eccab 
Nate Coraor 61cf27e 
jeremy goecks 52ead67 

jeremy goecks c3eccab 


James Taylor 811eda3 
jeremy goecks c3eccab 






jeremy goecks 146eae8 




jeremy goecks 55af252 
jeremy goecks c3eccab 

Kanwei Li 2ac1139 

jeremy goecks f8a94b3 
Kanwei Li 2ac1139 
jeremy goecks f8a94b3 



jeremy goecks 8a37300 
jeremy goecks f8a94b3 
jeremy goecks 8a37300 
Kanwei Li 2ac1139 

jeremy goecks 8a37300 
Kanwei Li 2ac1139 
jeremy goecks 8a37300 
Kanwei Li 2ac1139 
jeremy goecks 8a37300 
Kanwei Li 2ac1139 


jeremy goecks 96ec861 
jeremy goecks c3eccab 

































jeremy goecks 146eae8 


















jeremy goecks de83b37 









jeremy goecks 270d4b7 
jeremy goecks de83b37 
jeremy goecks 270d4b7 
jeremy goecks de83b37 

jeremy goecks 146eae8 

















jeremy goecks 55af252 
jeremy goecks 8a37300 
jeremy goecks 55af252 
jeremy goecks 8a37300 
jeremy goecks 55af252 

jeremy goecks 146eae8 








jeremy goecks c3eccab 







Kanwei Li 77753ee 
jeremy goecks c3eccab 
























jeremy goecks 4bd0731 
jeremy goecks c3eccab 


jeremy goecks 4bd0731 
jeremy goecks c3eccab 

Kanwei Li 77753ee 
jeremy goecks c3eccab 
























jeremy goecks 4bd0731 
jeremy goecks c3eccab 


jeremy goecks 4bd0731 

jeremy goecks c3eccab 











jeremy goecks 2cf1ad5 
jeremy goecks c3eccab 
Nate Coraor 61cf27e 
jeremy goecks 2cf1ad5 
jeremy goecks de83b37 


jeremy goecks 270d4b7 
jeremy goecks de83b37 



jeremy goecks 270d4b7 
jeremy goecks de83b37 
jeremy goecks 2cf1ad5 
jeremy goecks de83b37 



jeremy goecks c3eccab 





Kanwei Li 77753ee 
jeremy goecks c3eccab 








Kanwei Li 692458d 

Kanwei Li 77753ee 
Kanwei Li 692458d 






jeremy goecks c3eccab 


jeremy goecks 0bbf30a 
jeremy goecks c3eccab 


















Kanwei Li 77753ee 

jeremy goecks c3eccab 
jeremy goecks 270d4b7 
jeremy goecks c3eccab 
Kanwei Li 77753ee 
jeremy goecks c3eccab 


Kanwei Li 77753ee 

jeremy goecks c3eccab 

Kanwei Li 77753ee 
jeremy goecks c3eccab 

Kanwei Li 77753ee 

jeremy goecks c3eccab 
Kanwei Li 77753ee 
jeremy goecks c3eccab 
jeremy goecks 0bbf30a 
jeremy goecks c3eccab 








jeremy goecks 0bbf30a 
jeremy goecks c3eccab 









Kanwei Li 77753ee 
jeremy goecks c3eccab 
Kanwei Li 77753ee 
jeremy goecks c3eccab 







Kanwei Li 77753ee 
jeremy goecks c3eccab 



jeremy goecks c0eb252 

jeremy goecks 270d4b7 
jeremy goecks c3eccab 








jeremy goecks 270d4b7 
jeremy goecks c3eccab 


Kanwei Li 77753ee 
jeremy goecks c3eccab 









Kanwei Li 77753ee 
jeremy goecks 1291bc3 

Nate Coraor 61cf27e 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
from galaxy import model
from galaxy.model.item_attrs import *
from galaxy.web.base.controller import *
from galaxy.web.framework.helpers import time_ago, grids, iff
from galaxy.util.sanitize_html import sanitize_html


class VisualizationListGrid( grids.Grid ):
    # Grid definition
    title = "Saved Visualizations"
    model_class = model.Visualization
    default_sort_key = "-update_time"
    default_filter = dict( title="All", deleted="False", tags="All", sharing="All" )
    columns = [
        grids.TextColumn( "Title", key="title", attach_popup=True,
                         link=( lambda item: dict( controller="tracks", action="browser", id=item.id ) ) ),
        grids.TextColumn( "Dbkey", key="dbkey" ),
        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ),
        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
        grids.GridColumn( "Created", key="create_time", format=time_ago ),
        grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
    ]    
    columns.append( 
        grids.MulticolFilterColumn(  
        "Search", 
        cols_to_filter=[ columns[0], columns[2] ], 
        key="free-text-search", visible=False, filterable="standard" )
                )
    global_actions = [
        grids.GridAction( "Create new visualization", dict( action='create' ) )
    ]
    operations = [
        grids.GridOperation( "View/Edit", allow_multiple=False, url_args=dict( controller='tracks', action='browser' ) ),
        grids.GridOperation( "Edit Attributes", allow_multiple=False, url_args=dict( action='edit') ),
        grids.GridOperation( "Copy", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False, url_args=dict( action='clone') ),
        grids.GridOperation( "Share or Publish", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ),
        grids.GridOperation( "Delete", condition=( lambda item: not item.deleted ), async_compatible=True, confirm="Are you sure you want to delete this visualization?" ),
    ]
    def apply_query_filter( self, trans, query, **kwargs ):
        return query.filter_by( user=trans.user, deleted=False )
        
class VisualizationAllPublishedGrid( grids.Grid ):
    # Grid definition
    use_panels = True
    use_async = True
    title = "Published Visualizations"
    model_class = model.Visualization
    default_sort_key = "update_time"
    default_filter = dict( title="All", username="All" )
    columns = [
        grids.PublicURLColumn( "Title", key="title", filterable="advanced" ),
        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.VisualizationAnnotationAssociation, filterable="advanced" ),
        grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced" ),
        grids.CommunityRatingColumn( "Community Rating", key="rating" ), 
        grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationAllPublishedGrid" ),
        grids.ReverseSortColumn( "Last Updated", key="update_time", format=time_ago )
    ]
    columns.append( 
        grids.MulticolFilterColumn(  
        "Search title, annotation, owner, and tags", 
        cols_to_filter=[ columns[0], columns[1], columns[2], columns[4] ], 
        key="free-text-search", visible=False, filterable="standard" )
                )
    def build_initial_query( self, trans, **kwargs ):
        # Join so that searching history.user makes sense.
        return trans.sa_session.query( self.model_class ).join( model.User.table )
    def apply_query_filter( self, trans, query, **kwargs ):
        return query.filter( self.model_class.deleted==False ).filter( self.model_class.published==True )


class VisualizationController( BaseUIController, Sharable, UsesAnnotations, 
                                UsesHistoryDatasetAssociation, UsesVisualization, 
                                UsesItemRatings ):
    _user_list_grid = VisualizationListGrid()
    _published_list_grid = VisualizationAllPublishedGrid()
    
    @web.expose
    def list_published( self, trans, *args, **kwargs ):
        grid = self._published_list_grid( trans, **kwargs )
        if 'async' in kwargs:
            return grid
        else:
            # Render grid wrapped in panels
            return trans.fill_template( "visualization/list_published.mako", grid=grid )

    @web.expose
    @web.require_login( "use Galaxy visualizations", use_panels=True )
    def index( self, trans, *args, **kwargs ):
        """ Lists user's saved visualizations. """
        return self.list( trans, *args, **kwargs )
    
    @web.expose
    @web.require_login()
    def clone(self, trans, id, *args, **kwargs):
        visualization = self.get_visualization( trans, id, check_ownership=False )            
        user = trans.get_user()
        owner = ( visualization.user == user )
        new_title = "Copy of '%s'" % visualization.title
        if not owner:
            new_title += " shared by %s" % visualization.user.email
            
        cloned_visualization = visualization.copy( user=trans.user, title=new_title )
        
        # Persist
        session = trans.sa_session
        session.add( cloned_visualization )
        session.flush()
        
        # Display the management page
        trans.set_message( 'Copy created with name "%s"' % cloned_visualization.title )
        return self.list( trans )
        
    @web.expose
    @web.require_login( "use Galaxy visualizations", use_panels=True )
    def list( self, trans, *args, **kwargs ):
        # Handle operation
        if 'operation' in kwargs and 'id' in kwargs:
            session = trans.sa_session
            operation = kwargs['operation'].lower()
            ids = util.listify( kwargs['id'] )
            for id in ids:
                item = session.query( model.Visualization ).get( trans.security.decode_id( id ) )
                if operation == "delete":
                    item.deleted = True
                if operation == "share or publish":
                    return self.sharing( trans, **kwargs )
            session.flush()
            
        # Build list of visualizations shared with user.
        shared_by_others = trans.sa_session \
            .query( model.VisualizationUserShareAssociation ) \
            .filter_by( user=trans.get_user() ) \
            .join( model.Visualization.table ) \
            .filter( model.Visualization.deleted == False ) \
            .order_by( desc( model.Visualization.update_time ) ) \
            .all()
        
        return trans.fill_template( "visualization/list.mako", grid=self._user_list_grid( trans, *args, **kwargs ), shared_by_others=shared_by_others )
        
    @web.expose
    @web.require_login( "modify Galaxy visualizations" )
    def set_slug_async( self, trans, id, new_slug ):
        """ Set item slug asynchronously. """
        visualization = self.get_visualization( trans, id )
        if visualization:
            visualization.slug = new_slug
            trans.sa_session.flush()
            return visualization.slug
            
    @web.expose
    @web.require_login( "use Galaxy visualizations" )
    def set_accessible_async( self, trans, id=None, accessible=False ):
        """ Set visualization's importable attribute and slug. """
        visualization = self.get_visualization( trans, id )

        # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed.
        importable = accessible in ['True', 'true', 't', 'T'];
        if visualization and visualization.importable != importable:
            if importable:
                self._make_item_accessible( trans.sa_session, visualization )
            else:
                visualization.importable = importable
            trans.sa_session.flush()

        return
        
    @web.expose
    @web.require_login( "rate items" )
    @web.json
    def rate_async( self, trans, id, rating ):
        """ Rate a visualization asynchronously and return updated community data. """

        visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True )
        if not visualization:
            return trans.show_error_message( "The specified visualization does not exist." )

        # Rate visualization.
        visualization_rating = self.rate_item( trans.sa_session, trans.get_user(), visualization, rating )

        return self.get_ave_item_rating_data( trans.sa_session, visualization )
        
    @web.expose
    @web.require_login( "share Galaxy visualizations" )
    def imp( self, trans, id ):
        """ Import a visualization into user's workspace. """
        # Set referer message.
        referer = trans.request.referer
        if referer is not "":
            referer_message = "<a href='%s'>return to the previous page</a>" % referer
        else:
            referer_message = "<a href='%s'>go to Galaxy's start page</a>" % url_for( '/' )
                    
        # Do import.
        session = trans.sa_session
        visualization = self.get_visualization( trans, id, check_ownership=False )
        if visualization.importable == False:
            return trans.show_error_message( "The owner of this visualization has disabled imports via this link.<br>You can %s" % referer_message, use_panels=True )
        elif visualization.deleted:
            return trans.show_error_message( "You can't import this visualization because it has been deleted.<br>You can %s" % referer_message, use_panels=True )
        else:
            # Create imported visualization via copy. 
            #   TODO: need to handle custom db keys.
            
            imported_visualization = visualization.copy( user=trans.user, title="imported: " + visualization.title )
            
            # Persist
            session = trans.sa_session
            session.add( imported_visualization )
            session.flush()
            
            # Redirect to load galaxy frames.
            return trans.show_ok_message(
                message="""Visualization "%s" has been imported. <br>You can <a href="%s">start using this visualization</a> or %s.""" 
                % ( visualization.title, web.url_for( controller='visualization' ), referer_message ), use_panels=True )
        

    @web.expose
    @web.require_login( "share Galaxy visualizations" )
    def sharing( self, trans, id, **kwargs ):
        """ Handle visualization sharing. """

        # Get session and visualization.
        session = trans.sa_session
        visualization = self.get_visualization( trans, id, check_ownership=True )

        # Do operation on visualization.
        if 'make_accessible_via_link' in kwargs:
            self._make_item_accessible( trans.sa_session, visualization )
        elif 'make_accessible_and_publish' in kwargs:
            self._make_item_accessible( trans.sa_session, visualization )
            visualization.published = True
        elif 'publish' in kwargs:
            visualization.published = True
        elif 'disable_link_access' in kwargs:
            visualization.importable = False
        elif 'unpublish' in kwargs:
            visualization.published = False
        elif 'disable_link_access_and_unpublish' in kwargs:
            visualization.importable = visualization.published = False
        elif 'unshare_user' in kwargs:
            user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) )
            if not user:
                error( "User not found for provided id" )
            association = session.query( model.VisualizationUserShareAssociation ) \
                                 .filter_by( user=user, visualization=visualization ).one()
            session.delete( association )

        session.flush()

        return trans.fill_template( "/sharing_base.mako", item=visualization, use_panels=True )

    @web.expose
    @web.require_login( "share Galaxy visualizations" )
    def share( self, trans, id=None, email="", use_panels=False ):
        """ Handle sharing a visualization with a particular user. """
        msg = mtype = None
        visualization = self.get_visualization( trans, id, check_ownership=True )
        if email:
            other = trans.sa_session.query( model.User ) \
                                    .filter( and_( model.User.table.c.email==email,
                                                   model.User.table.c.deleted==False ) ) \
                                    .first()
            if not other:
                mtype = "error"
                msg = ( "User '%s' does not exist" % email )
            elif other == trans.get_user():
                mtype = "error"
                msg = ( "You cannot share a visualization with yourself" )
            elif trans.sa_session.query( model.VisualizationUserShareAssociation ) \
                    .filter_by( user=other, visualization=visualization ).count() > 0:
                mtype = "error"
                msg = ( "Visualization already shared with '%s'" % email )
            else:
                share = model.VisualizationUserShareAssociation()
                share.visualization = visualization
                share.user = other
                session = trans.sa_session
                session.add( share )
                self.create_item_slug( session, visualization )
                session.flush()
                trans.set_message( "Visualization '%s' shared with user '%s'" % ( visualization.title, other.email ) )
                return trans.response.send_redirect( url_for( action='sharing', id=id ) )
        return trans.fill_template( "/ind_share_base.mako",
                                    message = msg,
                                    messagetype = mtype,
                                    item=visualization,
                                    email=email,
                                    use_panels=use_panels )
        

    @web.expose
    def display_by_username_and_slug( self, trans, username, slug ):
        """ Display visualization based on a username and slug. """

        # Get visualization.
        session = trans.sa_session
        user = session.query( model.User ).filter_by( username=username ).first()
        visualization = trans.sa_session.query( model.Visualization ).filter_by( user=user, slug=slug, deleted=False ).first()
        if visualization is None:
            raise web.httpexceptions.HTTPNotFound()
        
        # Security check raises error if user cannot access visualization.
        self.security_check( trans, visualization, False, True)
        
        # Get rating data.
        user_item_rating = 0
        if trans.get_user():
            user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), visualization )
            if user_item_rating:
                user_item_rating = user_item_rating.rating
            else:
                user_item_rating = 0
        ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, visualization )
        
        # Display.
        visualization_config = self.get_visualization_config( trans, visualization )
        return trans.stream_template_mako( "visualization/display.mako", item = visualization, item_data = visualization_config, 
                                            user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings,
                                            content_only=True )
        
    @web.expose
    @web.json
    @web.require_login( "get item name and link" )
    def get_name_and_link_async( self, trans, id=None ):
        """ Returns visualization's name and link. """
        visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True )

        if self.create_item_slug( trans.sa_session, visualization ):
            trans.sa_session.flush()
        return_dict = { "name" : visualization.title, "link" : url_for( action="display_by_username_and_slug", username=visualization.user.username, slug=visualization.slug ) }
        return return_dict

    @web.expose
    def get_item_content_async( self, trans, id ):
        """ Returns item content in HTML format. """
        
        # Get visualization, making sure it's accessible.
        visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True )
        if visualization is None:
            raise web.httpexceptions.HTTPNotFound()
        
        # Return content.
        visualization_config = self.get_visualization_config( trans, visualization )    
        return trans.fill_template_mako( "visualization/item_content.mako", encoded_id=trans.security.encode_id(visualization.id), 
                                            item=visualization, item_data=visualization_config, content_only=True )
        
    @web.expose
    @web.require_login( "create visualizations" )
    def create( self, trans, visualization_title="", visualization_slug="", visualization_annotation="", visualization_dbkey="" ):
        """
        Create a new visualization
        """
        user = trans.get_user()
        visualization_title_err = visualization_slug_err = visualization_annotation_err = ""
        if trans.request.method == "POST":
            if not visualization_title:
                visualization_title_err = "visualization name is required"
            elif not visualization_slug:
                visualization_slug_err = "visualization id is required"
            elif not VALID_SLUG_RE.match( visualization_slug ):
                visualization_slug_err = "visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
            elif trans.sa_session.query( model.Visualization ).filter_by( user=user, slug=visualization_slug, deleted=False ).first():
                visualization_slug_err = "visualization id must be unique"
            else:
                # Create the new stored visualization
                visualization = model.Visualization()
                visualization.title = visualization_title
                visualization.slug = visualization_slug
                visualization.dbkey = visualization_dbkey
                visualization.type = 'trackster' # HACK: set visualization type to trackster since it's the only viz
                visualization_annotation = sanitize_html( visualization_annotation, 'utf-8', 'text/html' )
                self.add_item_annotation( trans.sa_session, trans.get_user(), visualization, visualization_annotation )
                visualization.user = user
                
                # And the first (empty) visualization revision
                visualization_revision = model.VisualizationRevision()
                visualization_revision.title = visualization_title
                visualization_revision.config = {}
                visualization_revision.dbkey = visualization_dbkey
                visualization_revision.visualization = visualization
                visualization.latest_revision = visualization_revision

                # Persist
                session = trans.sa_session
                session.add(visualization)
                session.add(visualization_revision)
                session.flush()

                return trans.response.send_redirect( web.url_for( action='list' ) )
                                
        return trans.show_form( 
            web.FormBuilder( web.url_for(), "Create new visualization", submit_text="Submit" )
                .add_text( "visualization_title", "Visualization title", value=visualization_title, error=visualization_title_err )
                .add_text( "visualization_slug", "Visualization identifier", value=visualization_slug, error=visualization_slug_err,
                           help="""A unique identifier that will be used for
                                public links to this visualization. A default is generated
                                from the visualization title, but can be edited. This field
                                must contain only lowercase letters, numbers, and
                                the '-' character.""" )
                .add_select( "visualization_dbkey", "Visualization DbKey/Build", value=visualization_dbkey, options=self._get_dbkeys( trans ), error=None)
                .add_text( "visualization_annotation", "Visualization annotation", value=visualization_annotation, error=visualization_annotation_err,
                            help="A description of the visualization; annotation is shown alongside published visualizations."),
                template="visualization/create.mako" )
        
    @web.expose
    @web.require_login( "edit visualizations" )
    def edit( self, trans, id, visualization_title="", visualization_slug="", visualization_annotation="" ):
        """
        Edit a visualization's attributes.
        """
        visualization = self.get_visualization( trans, id, check_ownership=True )
        session = trans.sa_session
        
        visualization_title_err = visualization_slug_err = visualization_annotation_err = ""
        if trans.request.method == "POST":
            if not visualization_title:
                visualization_title_err = "Visualization name is required"
            elif not visualization_slug:
                visualization_slug_err = "Visualization id is required"
            elif not VALID_SLUG_RE.match( visualization_slug ):
                visualization_slug_err = "Visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
            elif visualization_slug != visualization.slug and trans.sa_session.query( model.Visualization ).filter_by( user=visualization.user, slug=visualization_slug, deleted=False ).first():
                visualization_slug_err = "Visualization id must be unique"
            else:
                visualization.title = visualization_title
                visualization.slug = visualization_slug
                if visualization_annotation != "":
                    visualization_annotation = sanitize_html( visualization_annotation, 'utf-8', 'text/html' )
                    self.add_item_annotation( trans.sa_session, trans.get_user(), visualization, visualization_annotation )
                session.flush()
                # Redirect to visualization list.
                return trans.response.send_redirect( web.url_for( action='list' ) )
        else:
            visualization_title = visualization.title
            # Create slug if it's not already set.
            if visualization.slug is None:
                self.create_item_slug( trans.sa_session, visualization )
            visualization_slug = visualization.slug
            visualization_annotation = self.get_item_annotation_str( trans.sa_session, trans.user, visualization )
            if not visualization_annotation:
                visualization_annotation = ""
        return trans.show_form( 
            web.FormBuilder( web.url_for( id=id ), "Edit visualization attributes", submit_text="Submit" )
                .add_text( "visualization_title", "Visualization title", value=visualization_title, error=visualization_title_err )
                .add_text( "visualization_slug", "Visualization identifier", value=visualization_slug, error=visualization_slug_err,
                           help="""A unique identifier that will be used for
                                public links to this visualization. A default is generated
                                from the visualization title, but can be edited. This field
                                must contain only lowercase letters, numbers, and
                                the '-' character.""" )
                .add_text( "visualization_annotation", "Visualization annotation", value=visualization_annotation, error=visualization_annotation_err,
                            help="A description of the visualization; annotation is shown alongside published visualizations."),
            template="visualization/create.mako" )

    def get_item( self, trans, id ):
        return self.get_visualization( trans, id )