root/galaxy-central/lib/galaxy/model/__init__.py @ 2

リビジョン 2, 84.3 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

行番号 
1"""
2Galaxy data model classes
3
4Naming: try to use class names that have a distinct plural form so that
5the relationship cardinalities are obvious (e.g. prefer Dataset to Data)
6"""
7
8import galaxy.datatypes
9from galaxy.util.bunch import Bunch
10from galaxy import util
11import galaxy.datatypes.registry
12from galaxy.datatypes.metadata import MetadataCollection
13from galaxy.security import RBACAgent, get_permitted_actions
14from galaxy.util.hash_util import *
15from galaxy.web.form_builder import *
16from galaxy.model.item_attrs import UsesAnnotations
17from sqlalchemy.orm import object_session
18import os.path, os, errno, codecs, operator, smtplib, socket, pexpect, logging
19
20log = logging.getLogger( __name__ )
21
22datatypes_registry = galaxy.datatypes.registry.Registry() #Default Value Required for unit tests
23
24def set_datatypes_registry( d_registry ):
25    """
26    Set up datatypes_registry
27    """
28    global datatypes_registry
29    datatypes_registry = d_registry
30
31class User( object ):
32    def __init__( self, email=None, password=None ):
33        self.email = email
34        self.password = password
35        self.external = False
36        self.deleted = False
37        self.purged = False
38        self.username = None
39        # Relationships
40        self.histories = []
41        self.credentials = []
42    def set_password_cleartext( self, cleartext ):
43        """Set 'self.password' to the digest of 'cleartext'."""
44        self.password = new_secure_hash( text_type=cleartext )
45    def check_password( self, cleartext ):
46        """Check if 'cleartext' matches 'self.password' when hashed."""
47        return self.password == new_secure_hash( text_type=cleartext )
48    def all_roles( self ):
49        roles = [ ura.role for ura in self.roles ]
50        for group in [ uga.group for uga in self.groups ]:
51            for role in [ gra.role for gra in group.roles ]:
52                if role not in roles:
53                    roles.append( role )
54        return roles
55    def accessible_libraries( self, trans, actions ):
56        # Get all permitted libraries for this user
57        all_libraries = trans.sa_session.query( trans.app.model.Library ) \
58                                        .filter( trans.app.model.Library.table.c.deleted == False ) \
59                                        .order_by( trans.app.model.Library.name )
60        roles = self.all_roles()
61        actions_to_check = actions
62        # The libraries dictionary looks like: { library : '1,2' }, library : '3' }
63        # Its keys are the libraries that should be displayed for the current user and whose values are a
64        # string of comma-separated folder ids, of the associated folders the should NOT be displayed.
65        # The folders that should not be displayed may not be a complete list, but it is ultimately passed
66        # to the calling method to keep from re-checking the same folders when the library / folder
67        # select lists are rendered.
68        libraries = {}
69        for library in all_libraries:
70            can_show, hidden_folder_ids = trans.app.security_agent.show_library_item( self, roles, library, actions_to_check )
71            if can_show:
72                libraries[ library ] = hidden_folder_ids
73        return libraries
74    def accessible_request_types( self, trans ):
75        active_request_types = trans.sa_session.query( trans.app.model.RequestType ) \
76                                               .filter( trans.app.model.RequestType.table.c.deleted == False ) \
77                                               .order_by( trans.app.model.RequestType.name )
78        # Filter active_request_types to those that can be accessed by this user
79        role_ids = [ r.id for r in self.all_roles() ]
80        accessible_request_types = set()
81        for request_type in active_request_types:
82            for permission in request_type.actions:
83                if permission.role.id in role_ids:
84                   accessible_request_types.add( request_type )
85        accessible_request_types = [ request_type for request_type in accessible_request_types ]
86        return accessible_request_types
87   
88class Job( object ):
89    """
90    A job represents a request to run a tool given input datasets, tool
91    parameters, and output datasets.
92    """
93    states = Bunch( NEW = 'new',
94                    UPLOAD = 'upload',
95                    WAITING = 'waiting',
96                    QUEUED = 'queued',
97                    RUNNING = 'running',
98                    OK = 'ok',
99                    ERROR = 'error',
100                    DELETED = 'deleted' )
101    def __init__( self ):
102        self.session_id = None
103        self.user_id = None
104        self.tool_id = None
105        self.tool_version = None
106        self.command_line = None
107        self.param_filename = None
108        self.parameters = []
109        self.input_datasets = []
110        self.output_datasets = []
111        self.output_library_datasets = []
112        self.state = Job.states.NEW
113        self.info = None
114        self.job_runner_name = None
115        self.job_runner_external_id = None
116        self.post_job_actions = []
117        self.imported = False
118       
119    def add_parameter( self, name, value ):
120        self.parameters.append( JobParameter( name, value ) )
121    def add_input_dataset( self, name, dataset ):
122        self.input_datasets.append( JobToInputDatasetAssociation( name, dataset ) )
123    def add_output_dataset( self, name, dataset ):
124        self.output_datasets.append( JobToOutputDatasetAssociation( name, dataset ) )
125    def add_output_library_dataset( self, name, dataset ):
126        self.output_library_datasets.append( JobToOutputLibraryDatasetAssociation( name, dataset ) )
127    def add_post_job_action(self, pja):
128        self.post_job_actions.append( PostJobActionAssociation( pja, self ) )
129    def set_state( self, state ):
130        self.state = state
131        # For historical reasons state propogates down to datasets
132        for da in self.output_datasets:
133            da.dataset.state = state
134    def get_param_values( self, app ):
135        """
136        Read encoded parameter values from the database and turn back into a
137        dict of tool parameter values.
138        """
139        param_dict = dict( [ ( p.name, p.value ) for p in self.parameters ] )
140        tool = app.toolbox.tools_by_id[self.tool_id]
141        param_dict = tool.params_from_strings( param_dict, app )
142        return param_dict
143    def check_if_output_datasets_deleted( self ):
144        """
145        Return true if all of the output datasets associated with this job are
146        in the deleted state
147        """
148        for dataset_assoc in self.output_datasets:
149            dataset = dataset_assoc.dataset
150            # only the originator of the job can delete a dataset to cause
151            # cancellation of the job, no need to loop through history_associations
152            if not dataset.deleted:
153                return False
154        return True
155    def mark_deleted( self ):
156        """
157        Mark this job as deleted, and mark any output datasets as discarded.
158        """
159        self.state = Job.states.DELETED
160        self.info = "Job output deleted by user before job completed."
161        for dataset_assoc in self.output_datasets:
162            dataset = dataset_assoc.dataset
163            dataset.deleted = True
164            dataset.state = dataset.states.DISCARDED
165            for dataset in dataset.dataset.history_associations:
166                # propagate info across shared datasets
167                dataset.deleted = True
168                dataset.blurb = 'deleted'
169                dataset.peek = 'Job deleted'
170                dataset.info = 'Job output deleted by user before job completed'
171
172class JobParameter( object ):
173    def __init__( self, name, value ):
174        self.name = name
175        self.value = value
176         
177class JobToInputDatasetAssociation( object ):
178    def __init__( self, name, dataset ):
179        self.name = name
180        self.dataset = dataset
181       
182class JobToOutputDatasetAssociation( object ):
183    def __init__( self, name, dataset ):
184        self.name = name
185        self.dataset = dataset
186
187class JobToOutputLibraryDatasetAssociation( object ):
188    def __init__( self, name, dataset ):
189        self.name = name
190        self.dataset = dataset
191
192class PostJobAction( object ):
193    def __init__( self, action_type, workflow_step, output_name = None, action_arguments = None):
194        self.action_type = action_type
195        self.output_name = output_name
196        self.action_arguments = action_arguments
197        self.workflow_step = workflow_step
198       
199class PostJobActionAssociation( object ):
200    def __init__(self, pja, job):
201        self.job = job
202        self.post_job_action = pja
203
204class JobExternalOutputMetadata( object ):
205    def __init__( self, job = None, dataset = None ):
206        self.job = job
207        if isinstance( dataset, galaxy.model.HistoryDatasetAssociation ):
208            self.history_dataset_association = dataset
209        elif isinstance( dataset, galaxy.model.LibraryDatasetDatasetAssociation ):
210            self.library_dataset_dataset_association = dataset
211    @property
212    def dataset( self ):
213        if self.history_dataset_association:
214            return self.history_dataset_association
215        elif self.library_dataset_dataset_association:
216            return self.library_dataset_dataset_association
217        return None
218       
219class JobExportHistoryArchive( object ):
220    def __init__( self, job=None, history=None, dataset=None, compressed=False, \
221                  history_attrs_filename=None, datasets_attrs_filename=None,
222                  jobs_attrs_filename=None ):
223        self.job = job
224        self.history = history
225        self.dataset = dataset
226        self.compressed = compressed
227        self.history_attrs_filename = history_attrs_filename
228        self.datasets_attrs_filename = datasets_attrs_filename
229        self.jobs_attrs_filename = jobs_attrs_filename
230
231class Group( object ):
232    def __init__( self, name = None ):
233        self.name = name
234        self.deleted = False
235
236class UserGroupAssociation( object ):
237    def __init__( self, user, group ):
238        self.user = user
239        self.group = group
240
241class History( object, UsesAnnotations ):
242    def __init__( self, id=None, name=None, user=None ):
243        self.id = id
244        self.name = name or "Unnamed history"
245        self.deleted = False
246        self.purged = False
247        self.genome_build = None
248        self.published = False
249        # Relationships
250        self.user = user
251        self.datasets = []
252        self.galaxy_sessions = []
253    def _next_hid( self ):
254        # TODO: override this with something in the database that ensures
255        # better integrity
256        if len( self.datasets ) == 0:
257            return 1
258        else:
259            last_hid = 0
260            for dataset in self.datasets:
261                if dataset.hid > last_hid:
262                    last_hid = dataset.hid
263            return last_hid + 1
264    def add_galaxy_session( self, galaxy_session, association=None ):
265        if association is None:
266            self.galaxy_sessions.append( GalaxySessionToHistoryAssociation( galaxy_session, self ) )
267        else:
268            self.galaxy_sessions.append( association )
269    def add_dataset( self, dataset, parent_id=None, genome_build=None, set_hid = True ):
270        if isinstance( dataset, Dataset ):
271            dataset = HistoryDatasetAssociation( dataset = dataset, copied_from = dataset )
272            object_session( self ).add( dataset )
273            object_session( self ).flush()
274        elif not isinstance( dataset, HistoryDatasetAssociation ):
275            raise TypeError, "You can only add Dataset and HistoryDatasetAssociation instances to a history ( you tried to add %s )." % str( dataset )
276        if parent_id:
277            for data in self.datasets:
278                if data.id == parent_id:
279                    dataset.hid = data.hid
280                    break
281            else:
282                if set_hid:
283                    dataset.hid = self._next_hid()
284        else:
285            if set_hid:
286                dataset.hid = self._next_hid()
287        dataset.history = self
288        if genome_build not in [None, '?']:
289            self.genome_build = genome_build
290        self.datasets.append( dataset )
291    def copy( self, name=None, target_user=None, activatable=False ):
292        # Create new history.
293        if not name:
294            name = self.name
295        if not target_user:
296            target_user = self.user
297        new_history = History( name=name, user=target_user )
298        db_session = object_session( self )
299        db_session.add( new_history )
300        db_session.flush()
301       
302        # Copy annotation.
303        self.copy_item_annotation( db_session, self.user, self, target_user, new_history )
304
305        # Copy HDAs.
306        if activatable:
307            hdas = self.activatable_datasets
308        else:
309            hdas = self.active_datasets
310        for hda in hdas:
311            # Copy HDA.
312            new_hda = hda.copy( copy_children=True, target_history=new_history )
313            new_history.add_dataset( new_hda, set_hid = False )
314            db_session.add( new_hda )
315            db_session.flush()           
316            # Copy annotation.
317            self.copy_item_annotation( db_session, self.user, hda, target_user, new_hda )
318        new_history.hid_counter = self.hid_counter
319        db_session.add( new_history )
320        db_session.flush()
321        return new_history
322    @property
323    def activatable_datasets( self ):
324        # This needs to be a list
325        return [ hda for hda in self.datasets if not hda.dataset.deleted ]
326    def get_display_name( self ):
327        """ History name can be either a string or a unicode object. If string, convert to unicode object assuming 'utf-8' format. """
328        history_name = self.name
329        if isinstance(history_name, str):
330            history_name = unicode(history_name, 'utf-8')
331        return history_name
332
333class HistoryUserShareAssociation( object ):
334    def __init__( self ):
335        self.history = None
336        self.user = None
337
338class UserRoleAssociation( object ):
339    def __init__( self, user, role ):
340        self.user = user
341        self.role = role
342
343class GroupRoleAssociation( object ):
344    def __init__( self, group, role ):
345        self.group = group
346        self.role = role
347
348class Role( object ):
349    private_id = None
350    types = Bunch(
351        PRIVATE = 'private',
352        SYSTEM = 'system',
353        USER = 'user',
354        ADMIN = 'admin',
355        SHARING = 'sharing'
356    )
357    def __init__( self, name="", description="", type="system", deleted=False ):
358        self.name = name
359        self.description = description
360        self.type = type
361        self.deleted = deleted
362
363class DatasetPermissions( object ):
364    def __init__( self, action, dataset, role ):
365        self.action = action
366        self.dataset = dataset
367        self.role = role
368
369class LibraryPermissions( object ):
370    def __init__( self, action, library_item, role ):
371        self.action = action
372        if isinstance( library_item, Library ):
373            self.library = library_item
374        else:
375            raise "Invalid Library specified: %s" % library_item.__class__.__name__
376        self.role = role
377
378class LibraryFolderPermissions( object ):
379    def __init__( self, action, library_item, role ):
380        self.action = action
381        if isinstance( library_item, LibraryFolder ):
382            self.folder = library_item
383        else:
384            raise "Invalid LibraryFolder specified: %s" % library_item.__class__.__name__
385        self.role = role
386
387class LibraryDatasetPermissions( object ):
388    def __init__( self, action, library_item, role ):
389        self.action = action
390        if isinstance( library_item, LibraryDataset ):
391            self.library_dataset = library_item
392        else:
393            raise "Invalid LibraryDataset specified: %s" % library_item.__class__.__name__
394        self.role = role
395
396class LibraryDatasetDatasetAssociationPermissions( object ):
397    def __init__( self, action, library_item, role ):
398        self.action = action
399        if isinstance( library_item, LibraryDatasetDatasetAssociation ):
400            self.library_dataset_dataset_association = library_item
401        else:
402            raise "Invalid LibraryDatasetDatasetAssociation specified: %s" % library_item.__class__.__name__
403        self.role = role
404
405class DefaultUserPermissions( object ):
406    def __init__( self, user, action, role ):
407        self.user = user
408        self.action = action
409        self.role = role
410
411class DefaultHistoryPermissions( object ):
412    def __init__( self, history, action, role ):
413        self.history = history
414        self.action = action
415        self.role = role
416
417class Dataset( object ):
418    states = Bunch( NEW = 'new',
419                    UPLOAD = 'upload',
420                    QUEUED = 'queued',
421                    RUNNING = 'running',
422                    OK = 'ok',
423                    EMPTY = 'empty',
424                    ERROR = 'error',
425                    DISCARDED = 'discarded',
426                    SETTING_METADATA = 'setting_metadata',
427                    FAILED_METADATA = 'failed_metadata' )
428    permitted_actions = get_permitted_actions( filter='DATASET' )
429    file_path = "/tmp/"
430    engine = None
431    def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ):
432        self.id = id
433        self.state = state
434        self.deleted = False
435        self.purged = False
436        self.purgable = purgable
437        self.external_filename = external_filename
438        self._extra_files_path = extra_files_path
439        self.file_size = file_size
440    def get_file_name( self ):
441        if not self.external_filename:
442            assert self.id is not None, "ID must be set before filename used (commit the object)"
443            # First try filename directly under file_path
444            filename = os.path.join( self.file_path, "dataset_%d.dat" % self.id )
445            # Only use that filename if it already exists (backward compatibility),
446            # otherwise construct hashed path
447            if not os.path.exists( filename ):
448                dir = os.path.join( self.file_path, *directory_hash_id( self.id ) )
449                # Create directory if it does not exist
450                if not os.path.exists( dir ):
451                    os.makedirs( dir )
452                # Return filename inside hashed directory
453                return os.path.abspath( os.path.join( dir, "dataset_%d.dat" % self.id ) )
454        else:
455            filename = self.external_filename
456        # Make filename absolute
457        return os.path.abspath( filename )
458    def set_file_name ( self, filename ):
459        if not filename:
460            self.external_filename = None
461        else:
462            self.external_filename = filename
463    file_name = property( get_file_name, set_file_name )
464    @property
465    def extra_files_path( self ):
466        if self._extra_files_path:
467            path = self._extra_files_path
468        else:
469            path = os.path.join( self.file_path, "dataset_%d_files" % self.id )
470            #only use path directly under self.file_path if it exists
471            if not os.path.exists( path ):
472                path = os.path.join( os.path.join( self.file_path, *directory_hash_id( self.id ) ), "dataset_%d_files" % self.id )
473        # Make path absolute
474        return os.path.abspath( path )
475    def get_size( self, nice_size=False ):
476        """Returns the size of the data on disk"""
477        if self.file_size:
478            if nice_size:
479                return galaxy.datatypes.data.nice_size( self.file_size )
480            else:
481                return self.file_size
482        else:
483            try:
484                if nice_size:
485                    return galaxy.datatypes.data.nice_size( os.path.getsize( self.file_name ) )
486                else:
487                    return os.path.getsize( self.file_name )
488            except OSError:
489                return 0
490    def set_size( self ):
491        """Returns the size of the data on disk"""
492        try:
493            if not self.file_size:
494                self.file_size = os.path.getsize( self.file_name )
495        except OSError:
496            self.file_size = 0
497    def has_data( self ):
498        """Detects whether there is any data"""
499        return self.get_size() > 0
500    def mark_deleted( self, include_children=True ):
501        self.deleted = True
502    def is_multi_byte( self ):
503        if not self.has_data():
504            return False
505        try:
506            return util.is_multi_byte( codecs.open( self.file_name, 'r', 'utf-8' ).read( 100 ) )
507        except UnicodeDecodeError, e:
508            return False
509    # FIXME: sqlalchemy will replace this
510    def _delete(self):
511        """Remove the file that corresponds to this data"""
512        try:
513            os.remove(self.data.file_name)
514        except OSError, e:
515            log.critical('%s delete error %s' % (self.__class__.__name__, e))
516    def get_access_roles( self, trans ):
517        roles = []
518        for dp in self.actions:
519            if dp.action == trans.app.security_agent.permitted_actions.DATASET_ACCESS.action:
520                roles.append( dp.role )
521        return roles
522
523class DatasetInstance( object ):
524    """A base class for all 'dataset instances', HDAs, LDAs, etc"""
525    states = Dataset.states
526    permitted_actions = Dataset.permitted_actions
527    def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None,
528                  dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None,
529                  parent_id=None, validation_errors=None, visible=True, create_dataset=False, sa_session=None ):
530        self.name = name or "Unnamed dataset"
531        self.id = id
532        self.info = info
533        self.blurb = blurb
534        self.peek = peek
535        self.extension = extension
536        self.designation = designation
537        self.metadata = metadata or dict()
538        if dbkey: #dbkey is stored in metadata, only set if non-zero, or else we could clobber one supplied by input 'metadata'
539            self.dbkey = dbkey
540        self.deleted = deleted
541        self.visible = visible
542        # Relationships
543        if not dataset and create_dataset:
544            # Had to pass the sqlalchemy session in order to create a new dataset
545            dataset = Dataset( state=Dataset.states.NEW )
546            sa_session.add( dataset )
547            sa_session.flush()
548        self.dataset = dataset
549        self.parent_id = parent_id
550        self.validation_errors = validation_errors
551    @property
552    def ext( self ):
553        return self.extension
554    def get_dataset_state( self ):
555        #self._state is currently only used when setting metadata externally
556        #leave setting the state as-is, we'll currently handle this specially in the external metadata code
557        if self._state:
558            return self._state
559        return self.dataset.state
560    def set_dataset_state ( self, state ):
561        self.dataset.state = state
562        object_session( self ).add( self.dataset )
563        object_session( self ).flush() #flush here, because hda.flush() won't flush the Dataset object
564    state = property( get_dataset_state, set_dataset_state )
565    def get_file_name( self ):
566        return self.dataset.get_file_name()
567    def set_file_name (self, filename):
568        return self.dataset.set_file_name( filename )
569    file_name = property( get_file_name, set_file_name )
570    @property
571    def extra_files_path( self ):
572        return self.dataset.extra_files_path
573    @property
574    def datatype( self ):
575        return datatypes_registry.get_datatype_by_extension( self.extension )
576    def get_metadata( self ):
577        if not hasattr( self, '_metadata_collection' ) or self._metadata_collection.parent != self: #using weakref to store parent (to prevent circ ref), does a Session.clear() cause parent to be invalidated, while still copying over this non-database attribute?
578            self._metadata_collection = MetadataCollection( self )
579        return self._metadata_collection
580    def set_metadata( self, bunch ):
581        # Needs to accept a MetadataCollection, a bunch, or a dict
582        self._metadata = self.metadata.make_dict_copy( bunch )
583    metadata = property( get_metadata, set_metadata )
584    # This provide backwards compatibility with using the old dbkey
585    # field in the database.  That field now maps to "old_dbkey" (see mapping.py).
586    def get_dbkey( self ):
587        dbkey = self.metadata.dbkey
588        if not isinstance(dbkey, list): dbkey = [dbkey]
589        if dbkey in [[None], []]: return "?"
590        return dbkey[0]
591    def set_dbkey( self, value ):
592        if "dbkey" in self.datatype.metadata_spec:
593            if not isinstance(value, list):
594                self.metadata.dbkey = [value]
595            else:
596                self.metadata.dbkey = value
597    dbkey = property( get_dbkey, set_dbkey )
598    def change_datatype( self, new_ext ):
599        self.clear_associated_files()
600        datatypes_registry.change_datatype( self, new_ext )
601    def get_size( self, nice_size=False ):
602        """Returns the size of the data on disk"""
603        if nice_size:
604            return galaxy.datatypes.data.nice_size( self.dataset.get_size() )
605        return self.dataset.get_size()
606    def set_size( self ):
607        """Returns the size of the data on disk"""
608        return self.dataset.set_size()
609    def has_data( self ):
610        """Detects whether there is any data"""
611        return self.dataset.has_data()
612    def get_raw_data( self ):
613        """Returns the full data. To stream it open the file_name and read/write as needed"""
614        return self.datatype.get_raw_data( self )
615    def write_from_stream( self, stream ):
616        """Writes data from a stream"""
617        self.datatype.write_from_stream(self, stream)
618    def set_raw_data( self, data ):
619        """Saves the data on the disc"""
620        self.datatype.set_raw_data(self, data)
621    def get_mime( self ):
622        """Returns the mime type of the data"""
623        return datatypes_registry.get_mimetype_by_extension( self.extension.lower() )
624    def is_multi_byte( self ):
625        """Data consists of multi-byte characters"""
626        return self.dataset.is_multi_byte()
627    def set_peek( self, is_multi_byte=False ):
628        return self.datatype.set_peek( self, is_multi_byte=is_multi_byte )
629    def init_meta( self, copy_from=None ):
630        return self.datatype.init_meta( self, copy_from=copy_from )
631    def set_meta( self, **kwd ):
632        self.clear_associated_files( metadata_safe = True )
633        return self.datatype.set_meta( self, **kwd )
634    def missing_meta( self, **kwd ):
635        return self.datatype.missing_meta( self, **kwd )
636    def as_display_type( self, type, **kwd ):
637        return self.datatype.as_display_type( self, type, **kwd )
638    def display_peek( self ):
639        return self.datatype.display_peek( self )
640    def display_name( self ):
641        return self.datatype.display_name( self )
642    def display_info( self ):
643        return self.datatype.display_info( self )
644    def get_converted_files_by_type( self, file_type ):
645        for assoc in self.implicitly_converted_datasets:
646            if not assoc.deleted and assoc.type == file_type:
647                return assoc.dataset
648        return None
649    def get_converted_dataset(self, trans, target_ext):
650        """
651        Return converted dataset(s) if they exist. If not converted yet, do so and return None (the first time).
652        If unconvertible, raise exception.
653        """
654        # See if we can convert the dataset
655        if target_ext not in self.get_converter_types():
656            raise ValueError("Conversion from '%s' to '%s' not possible", self.extension, target_ext)
657       
658        # See if converted dataset already exists
659        converted_dataset = self.get_converted_files_by_type( target_ext )
660        if converted_dataset:
661            return converted_dataset
662       
663        # Conversion is possible but hasn't been done yet, run converter.
664        # Check if we have dependencies
665        deps = {}
666        try:
667            fail_dependencies = False
668            depends_on = trans.app.datatypes_registry.converter_deps[self.extension][target_ext]
669            for dependency in depends_on:
670                dep_dataset = self.get_converted_dataset(trans, dependency)
671                if dep_dataset is None or dep_dataset.state != trans.app.model.Job.states.OK:
672                    fail_dependencies = True
673                else:
674                    deps[dependency] = dep_dataset
675            if fail_dependencies:
676                return None
677        except ValueError:
678            raise ValueError("A dependency could not be converted.")
679        except KeyError:
680            pass # No deps
681           
682        assoc = ImplicitlyConvertedDatasetAssociation( parent=self, file_type=target_ext, metadata_safe=False )
683        new_dataset = self.datatype.convert_dataset( trans, self, target_ext, return_output=True, visible=False, deps=deps ).values()[0]
684        new_dataset.hid = self.hid
685        new_dataset.name = self.name
686        session = trans.sa_session
687        session.add( new_dataset )
688        assoc.dataset = new_dataset
689        session.add( assoc )
690        session.flush()
691        return None
692    def clear_associated_files( self, metadata_safe = False, purge = False ):
693        raise 'Unimplemented'
694    def get_child_by_designation(self, designation):
695        for child in self.children:
696            if child.designation == designation:
697                return child
698        return None
699    def get_converter_types(self):
700        return self.datatype.get_converter_types( self, datatypes_registry )
701    def find_conversion_destination( self, accepted_formats, **kwd ):
702        """Returns ( target_ext, existing converted dataset )"""
703        return self.datatype.find_conversion_destination( self, accepted_formats, datatypes_registry, **kwd )
704    def add_validation_error( self, validation_error ):
705        self.validation_errors.append( validation_error )
706    def extend_validation_errors( self, validation_errors ):
707        self.validation_errors.extend(validation_errors)
708    def mark_deleted( self, include_children=True ):
709        self.deleted = True
710        if include_children:
711            for child in self.children:
712                child.mark_deleted()
713    def mark_undeleted( self, include_children=True ):
714        self.deleted = False
715        if include_children:
716            for child in self.children:
717                child.mark_undeleted()
718    def mark_unhidden( self, include_children=True ):
719        self.visible = True
720        if include_children:
721            for child in self.children:
722                child.mark_unhidden()
723    def undeletable( self ):
724        if self.purged:
725            return False
726        return True
727    @property
728    def is_pending( self ):
729        """
730        Return true if the dataset is neither ready nor in error
731        """
732        return self.state in ( self.states.NEW, self.states.UPLOAD,
733                               self.states.QUEUED, self.states.RUNNING,
734                               self.states.SETTING_METADATA )
735    @property
736    def source_library_dataset( self ):
737        def get_source( dataset ):
738            if isinstance( dataset, LibraryDatasetDatasetAssociation ):
739                if dataset.library_dataset:
740                    return ( dataset, dataset.library_dataset )
741            if dataset.copied_from_library_dataset_dataset_association:
742                source = get_source( dataset.copied_from_library_dataset_dataset_association )
743                if source:
744                    return source
745            if dataset.copied_from_history_dataset_association:
746                source = get_source( dataset.copied_from_history_dataset_association )
747                if source:
748                    return source
749            return ( None, None )
750        return get_source( self )
751
752    def get_display_applications( self, trans ):
753        return self.datatype.get_display_applications_by_dataset( self, trans )
754
755class HistoryDatasetAssociation( DatasetInstance ):
756    def __init__( self,
757                  hid = None,
758                  history = None,
759                  copied_from_history_dataset_association = None,
760                  copied_from_library_dataset_dataset_association = None,
761                  sa_session = None,
762                  **kwd ):
763        # FIXME: sa_session is must be passed to DataSetInstance if the create_dataset
764        # parameter is True so that the new object can be flushed.  Is there a better way?
765        DatasetInstance.__init__( self, sa_session=sa_session, **kwd )
766        self.hid = hid
767        # Relationships
768        self.history = history
769        self.copied_from_history_dataset_association = copied_from_history_dataset_association
770        self.copied_from_library_dataset_dataset_association = copied_from_library_dataset_dataset_association
771    def copy( self, copy_children = False, parent_id = None, target_history = None ):
772        hda = HistoryDatasetAssociation( hid=self.hid,
773                                         name=self.name,
774                                         info=self.info,
775                                         blurb=self.blurb,
776                                         peek=self.peek,
777                                         extension=self.extension,
778                                         dbkey=self.dbkey,
779                                         dataset = self.dataset,
780                                         visible=self.visible,
781                                         deleted=self.deleted,
782                                         parent_id=parent_id,
783                                         copied_from_history_dataset_association=self,
784                                         history = target_history )
785        object_session( self ).add( hda )
786        object_session( self ).flush()
787        hda.set_size()
788        # Need to set after flushed, as MetadataFiles require dataset.id
789        hda.metadata = self.metadata
790        if copy_children:
791            for child in self.children:
792                child_copy = child.copy( copy_children = copy_children, parent_id = hda.id )
793        if not self.datatype.copy_safe_peek:
794            # In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs
795            hda.set_peek()
796        object_session( self ).flush()
797        return hda
798    def to_library_dataset_dataset_association( self, trans, target_folder, replace_dataset=None, parent_id=None, user=None, roles=[], ldda_message='' ):
799        if replace_dataset:
800            # The replace_dataset param ( when not None ) refers to a LibraryDataset that is being replaced with a new version.
801            library_dataset = replace_dataset
802        else:
803            # If replace_dataset is None, the Library level permissions will be taken from the folder and applied to the new
804            # LibraryDataset, and the current user's DefaultUserPermissions will be applied to the associated Dataset.
805            library_dataset = LibraryDataset( folder=target_folder, name=self.name, info=self.info )
806            object_session( self ).add( library_dataset )
807            object_session( self ).flush()
808        if not user:
809            # This should never happen since users must be authenticated to upload to a data library
810            user = self.history.user
811        ldda = LibraryDatasetDatasetAssociation( name=self.name,
812                                                 info=self.info,
813                                                 blurb=self.blurb,
814                                                 peek=self.peek,
815                                                 extension=self.extension,
816                                                 dbkey=self.dbkey,
817                                                 dataset=self.dataset,
818                                                 library_dataset=library_dataset,
819                                                 visible=self.visible,
820                                                 deleted=self.deleted,
821                                                 parent_id=parent_id,
822                                                 copied_from_history_dataset_association=self,
823                                                 user=user )
824        object_session( self ).add( ldda )
825        object_session( self ).flush()
826        # If roles were selected on the upload form, restrict access to the Dataset to those roles
827        for role in roles:
828            dp = trans.model.DatasetPermissions( trans.app.security_agent.permitted_actions.DATASET_ACCESS.action, ldda.dataset, role )
829            trans.sa_session.add( dp )
830            trans.sa_session.flush()
831        # Must set metadata after ldda flushed, as MetadataFiles require ldda.id
832        ldda.metadata = self.metadata
833        if ldda_message:
834            ldda.message = ldda_message
835        if not replace_dataset:
836            target_folder.add_library_dataset( library_dataset, genome_build=ldda.dbkey )
837            object_session( self ).add( target_folder )
838            object_session( self ).flush()
839        library_dataset.library_dataset_dataset_association_id = ldda.id
840        object_session( self ).add( library_dataset )
841        object_session( self ).flush()
842        for child in self.children:
843            child_copy = child.to_library_dataset_dataset_association( trans,
844                                                                       target_folder=target_folder,
845                                                                       replace_dataset=replace_dataset,
846                                                                       parent_id=ldda.id,
847                                                                       user=ldda.user )
848        if not self.datatype.copy_safe_peek:
849            # In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs
850            ldda.set_peek()
851        object_session( self ).flush()
852        return ldda
853    def clear_associated_files( self, metadata_safe = False, purge = False ):
854        # metadata_safe = True means to only clear when assoc.metadata_safe == False
855        for assoc in self.implicitly_converted_datasets:
856            if not metadata_safe or not assoc.metadata_safe:
857                assoc.clear( purge = purge )
858    def get_display_name( self ):
859        ## Name can be either a string or a unicode object. If string, convert to unicode object assuming 'utf-8' format.
860        hda_name = self.name
861        if isinstance(hda_name, str):
862            hda_name = unicode(hda_name, 'utf-8')
863        return hda_name
864    def get_access_roles( self, trans ):
865        return self.dataset.get_access_roles( trans )
866
867class HistoryDatasetAssociationDisplayAtAuthorization( object ):
868    def __init__( self, hda=None, user=None, site=None ):
869        self.history_dataset_association = hda
870        self.user = user
871        self.site = site
872
873class Library( object ):
874    permitted_actions = get_permitted_actions( filter='LIBRARY' )
875    api_collection_visible_keys = ( 'id', 'name' )
876    api_element_visible_keys = ( 'name', 'description', 'synopsis' )
877    def __init__( self, name=None, description=None, synopsis=None, root_folder=None ):
878        self.name = name or "Unnamed library"
879        self.description = description
880        self.synopsis = synopsis
881        self.root_folder = root_folder
882    def get_info_association( self, restrict=False, inherited=False ):
883        if self.info_association:
884            if not inherited or self.info_association[0].inheritable:
885                return self.info_association[0], inherited
886            else:
887                return None, inherited
888        return None, inherited
889    def get_template_widgets( self, trans, get_contents=True ):
890        # See if we have any associated templates - the returned value for
891        # inherited is not applicable at the library level.  The get_contents
892        # param is passed by callers that are inheriting a template - these
893        # are usually new library datsets for which we want to include template
894        # fields on the upload form, but not necessarily the contents of the
895        # inherited template saved for the parent.
896        info_association, inherited = self.get_info_association()
897        if info_association:
898            template = info_association.template
899            if get_contents:
900                # See if we have any field contents
901                info = info_association.info
902                if info:
903                    return template.get_widgets( trans.user, contents=info.content )
904            return template.get_widgets( trans.user )
905        return []
906    def get_access_roles( self, trans ):
907        roles = []
908        for lp in self.actions:
909            if lp.action == trans.app.security_agent.permitted_actions.LIBRARY_ACCESS.action:
910                roles.append( lp.role )
911        return roles
912    def get_display_name( self ):
913        # Library name can be either a string or a unicode object. If string,
914        # convert to unicode object assuming 'utf-8' format.
915        name = self.name
916        if isinstance( name, str ):
917            name = unicode( name, 'utf-8' )
918        return name
919    def get_api_value( self, view='collection' ):
920        rval = {}
921        try:
922            visible_keys = self.__getattribute__( 'api_' + view + '_visible_keys' )
923        except AttributeError:
924            raise Exception( 'Unknown API view: %s' % view )
925        for key in visible_keys:
926            try:
927                rval[key] = self.__getattribute__( key )
928            except AttributeError:
929                rval[key] = None
930        return rval
931
932class LibraryFolder( object ):
933    api_element_visible_keys = ( 'name', 'description', 'item_count', 'genome_build' )
934    def __init__( self, name=None, description=None, item_count=0, order_id=None ):
935        self.name = name or "Unnamed folder"
936        self.description = description
937        self.item_count = item_count
938        self.order_id = order_id
939        self.genome_build = None
940    def add_library_dataset( self, library_dataset, genome_build=None ):
941        library_dataset.folder_id = self.id
942        library_dataset.order_id = self.item_count
943        self.item_count += 1
944        if genome_build not in [None, '?']:
945            self.genome_build = genome_build
946    def add_folder( self, folder ):
947        folder.parent_id = self.id
948        folder.order_id = self.item_count
949        self.item_count += 1
950    def get_info_association( self, restrict=False, inherited=False ):
951        # If restrict is True, we will return this folder's info_association, not inheriting.
952        # If restrict is False, we'll return the next available info_association in the
953        # inheritable hierarchy if it is "inheritable".  True is also returned if the
954        # info_association was inherited and False if not.  This enables us to eliminate
955        # displaying any contents of the inherited template.
956        if self.info_association:
957            if not inherited or self.info_association[0].inheritable:
958                return self.info_association[0], inherited
959            else:
960                return None, inherited
961        if restrict:
962            return None, inherited
963        if self.parent:
964            return self.parent.get_info_association( inherited=True )
965        if self.library_root:
966            return self.library_root[0].get_info_association( inherited=True )
967        return None, inherited
968    def get_template_widgets( self, trans, get_contents=True ):
969        # See if we have any associated templates.  The get_contents
970        # param is passed by callers that are inheriting a template - these
971        # are usually new library datsets for which we want to include template
972        # fields on the upload form.
973        info_association, inherited = self.get_info_association()
974        if info_association:
975            if inherited:
976                template = info_association.template.current.latest_form
977            else:
978                template = info_association.template
979            # See if we have any field contents, but only if the info_association was
980            # not inherited ( we do not want to display the inherited contents ).
981            # (gvk: 8/30/10) Based on conversations with Dan, we agreed to ALWAYS inherit
982            # contents.  We'll use this behavior until we hear from the community that
983            # contents should not be inherited.  If we don't hear anything for a while,
984            # eliminate the old commented out behavior.
985            #if not inherited and get_contents:
986            if get_contents:
987                info = info_association.info
988                if info:
989                    return template.get_widgets( trans.user, info.content )
990            else:
991                return template.get_widgets( trans.user )
992        return []
993    @property
994    def active_library_datasets( self ):
995        def sort_by_attr( seq, attr ):
996            """
997            Sort the sequence of objects by object's attribute
998            Arguments:
999            seq  - the list or any sequence (including immutable one) of objects to sort.
1000            attr - the name of attribute to sort by
1001            """
1002            # Use the "Schwartzian transform"
1003            # Create the auxiliary list of tuples where every i-th tuple has form
1004            # (seq[i].attr, i, seq[i]) and sort it. The second item of tuple is needed not
1005            # only to provide stable sorting, but mainly to eliminate comparison of objects
1006            # (which can be expensive or prohibited) in case of equal attribute values.
1007            intermed = map( None, map( getattr, seq, ( attr, ) * len( seq ) ), xrange( len( seq ) ), seq )
1008            intermed.sort()
1009            return map( operator.getitem, intermed, ( -1, ) * len( intermed ) )
1010         # This needs to be a list
1011        active_library_datasets = [ ld for ld in self.datasets if ld.library_dataset_dataset_association and not ld.library_dataset_dataset_association.deleted ]
1012        return sort_by_attr( [ ld for ld in active_library_datasets ], 'name' )
1013    @property
1014    def activatable_library_datasets( self ):
1015         # This needs to be a list
1016        return [ ld for ld in self.datasets if ld.library_dataset_dataset_association and not ld.library_dataset_dataset_association.dataset.deleted ]
1017    @property
1018    def active_datasets( self ):
1019        # This needs to be a list
1020        return [ ld.library_dataset_dataset_association.dataset for ld in self.datasets if ld.library_dataset_dataset_association and not ld.library_dataset_dataset_association.deleted ]
1021    def get_display_name( self ):
1022        # Library folder name can be either a string or a unicode object. If string,
1023        # convert to unicode object assuming 'utf-8' format.
1024        name = self.name
1025        if isinstance( name, str ):
1026            name = unicode( name, 'utf-8' )
1027        return name
1028    def get_api_value( self, view='collection' ):
1029        rval = {}
1030        info_association, inherited = self.get_info_association()
1031        if info_association:
1032            if inherited:
1033                template = info_association.template.current.latest_form
1034            else:
1035                template = info_association.template
1036            rval['data_template'] = template.name
1037       
1038        try:
1039            visible_keys = self.__getattribute__( 'api_' + view + '_visible_keys' )
1040        except AttributeError:
1041            raise Exception( 'Unknown API view: %s' % view )
1042        for key in visible_keys:
1043            try:
1044                rval[key] = self.__getattribute__( key )
1045            except AttributeError:
1046                rval[key] = None
1047        return rval
1048    @property
1049    def parent_library( self ):
1050        f = self
1051        while f.parent:
1052            f = f.parent
1053        return f.library_root[0]
1054
1055class LibraryDataset( object ):
1056    # This class acts as a proxy to the currently selected LDDA
1057    upload_options = [ ( 'upload_file', 'Upload files' ),
1058                       ( 'upload_directory', 'Upload directory of files' ),
1059                       ( 'upload_paths', 'Upload files from filesystem paths' ),
1060                       ( 'import_from_history', 'Import datasets from your current history' ) ]
1061    def __init__( self, folder=None, order_id=None, name=None, info=None, library_dataset_dataset_association=None, **kwd ):
1062        self.folder = folder
1063        self.order_id = order_id
1064        self.name = name
1065        self.info = info
1066        self.library_dataset_dataset_association = library_dataset_dataset_association
1067    def set_library_dataset_dataset_association( self, ldda ):
1068        self.library_dataset_dataset_association = ldda
1069        ldda.library_dataset = self
1070        object_session( self ).add_all( ( ldda, self ) )
1071        object_session( self ).flush()
1072    def get_info( self ):
1073        if self.library_dataset_dataset_association:
1074            return self.library_dataset_dataset_association.info
1075        elif self._info:
1076            return self._info
1077        else:
1078            return 'no info'
1079    def set_info( self, info ):
1080        self._info = info
1081    info = property( get_info, set_info )
1082    def get_name( self ):
1083        if self.library_dataset_dataset_association:
1084            return self.library_dataset_dataset_association.name
1085        elif self._name:
1086            return self._name
1087        else:
1088            return 'Unnamed dataset'
1089    def set_name( self, name ):
1090        self._name = name
1091    name = property( get_name, set_name )
1092    def display_name( self ):
1093        self.library_dataset_dataset_association.display_name()
1094    def get_purged( self ):
1095        return self.library_dataset_dataset_association.dataset.purged
1096    def set_purged( self, purged ):
1097        if purged:
1098            raise Exception( "Not implemented" )
1099        if not purged and self.purged:
1100            raise Exception( "Cannot unpurge once purged" )
1101    purged = property( get_purged, set_purged )
1102    def get_api_value( self, view='collection' ):
1103        # Since this class is a proxy to rather complex attributes we want to
1104        # display in other objects, we can't use the simpler method used by
1105        # other model classes.
1106        ldda = self.library_dataset_dataset_association
1107        template_data = {}
1108        for temp_info in ldda.info_association:
1109            template = temp_info.template
1110            content = temp_info.info.content
1111            tmp_dict = {}
1112            for i, field in enumerate(template.fields):
1113                tmp_dict[field['label']] = content[i]
1114            template_data[template.name] = tmp_dict
1115       
1116        rval = dict( name = ldda.name,
1117                     file_name = ldda.file_name,
1118                     uploaded_by = ldda.user.email,
1119                     message = ldda.message,
1120                     date_uploaded = ldda.create_time.isoformat(),
1121                     file_size = int( ldda.get_size() ),
1122                     data_type = ldda.ext,
1123                     genome_build = ldda.dbkey,
1124                     misc_info = ldda.info,
1125                     misc_blurb = ldda.blurb,
1126                     template_data = template_data )
1127        for name, spec in ldda.metadata.spec.items():
1128            val = ldda.metadata.get( name )
1129            if isinstance( val, MetadataFile ):
1130                val = val.file_name
1131            elif isinstance( val, list ):
1132                val = ', '.join( val )
1133            rval['metadata_' + name] = val
1134        return rval
1135
1136class LibraryDatasetDatasetAssociation( DatasetInstance ):
1137    def __init__( self,
1138                  copied_from_history_dataset_association=None,
1139                  copied_from_library_dataset_dataset_association=None,
1140                  library_dataset=None,
1141                  user=None,
1142                  sa_session=None,
1143                  **kwd ):
1144        # FIXME: sa_session is must be passed to DataSetInstance if the create_dataset
1145        # parameter in kwd is True so that the new object can be flushed.  Is there a better way?
1146        DatasetInstance.__init__( self, sa_session=sa_session, **kwd )
1147        if copied_from_history_dataset_association:
1148            self.copied_from_history_dataset_association_id = copied_from_history_dataset_association.id
1149        if copied_from_library_dataset_dataset_association:
1150            self.copied_from_library_dataset_dataset_association_id = copied_from_library_dataset_dataset_association.id
1151        self.library_dataset = library_dataset
1152        self.user = user
1153    def to_history_dataset_association( self, target_history, parent_id = None, add_to_history = False ):
1154        hda = HistoryDatasetAssociation( name=self.name,
1155                                         info=self.info,
1156                                         blurb=self.blurb,
1157                                         peek=self.peek,
1158                                         extension=self.extension,
1159                                         dbkey=self.dbkey,
1160                                         dataset=self.dataset,
1161                                         visible=self.visible,
1162                                         deleted=self.deleted,
1163                                         parent_id=parent_id,
1164                                         copied_from_library_dataset_dataset_association=self,
1165                                         history=target_history )
1166        object_session( self ).add( hda )
1167        object_session( self ).flush()
1168        hda.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id
1169        if add_to_history and target_history:
1170            target_history.add_dataset( hda )
1171        for child in self.children:
1172            child_copy = child.to_history_dataset_association( target_history = target_history, parent_id = hda.id, add_to_history = False )
1173        if not self.datatype.copy_safe_peek:
1174            hda.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs
1175        object_session( self ).flush()
1176        return hda
1177    def copy( self, copy_children = False, parent_id = None, target_folder = None ):
1178        ldda = LibraryDatasetDatasetAssociation( name=self.name,
1179                                                 info=self.info,
1180                                                 blurb=self.blurb,
1181                                                 peek=self.peek,
1182                                                 extension=self.extension,
1183                                                 dbkey=self.dbkey,
1184                                                 dataset=self.dataset,
1185                                                 visible=self.visible,
1186                                                 deleted=self.deleted,
1187                                                 parent_id=parent_id,
1188                                                 copied_from_library_dataset_dataset_association=self,
1189                                                 folder=target_folder )
1190        object_session( self ).add( ldda )
1191        object_session( self ).flush()
1192         # Need to set after flushed, as MetadataFiles require dataset.id
1193        ldda.metadata = self.metadata
1194        if copy_children:
1195            for child in self.children:
1196                child_copy = child.copy( copy_children = copy_children, parent_id = ldda.id )
1197        if not self.datatype.copy_safe_peek:
1198             # In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs
1199            ldda.set_peek()
1200        object_session( self ).flush()
1201        return ldda
1202    def clear_associated_files( self, metadata_safe = False, purge = False ):
1203        return
1204    def get_access_roles( self, trans ):
1205        return self.dataset.get_access_roles( trans )
1206    def get_info_association( self, restrict=False, inherited=False ):
1207        # If restrict is True, we will return this ldda's info_association whether it
1208        # exists or not ( in which case None will be returned ).  If restrict is False,
1209        # we'll return the next available info_association in the inheritable hierarchy.
1210        # True is also returned if the info_association was inherited, and False if not.
1211        # This enables us to eliminate displaying any contents of the inherited template.
1212        if self.info_association:
1213            return self.info_association[0], inherited
1214        if restrict:
1215            return None, inherited
1216        return self.library_dataset.folder.get_info_association( inherited=True )
1217    def get_template_widgets( self, trans, get_contents=True ):
1218        # See if we have any associated templatesThe get_contents
1219        # param is passed by callers that are inheriting a template - these
1220        # are usually new library datsets for which we want to include template
1221        # fields on the upload form, but not necessarily the contents of the
1222        # inherited template saved for the parent.
1223        info_association, inherited = self.get_info_association()
1224        if info_association:
1225            if inherited:
1226                template = info_association.template.current.latest_form
1227            else:
1228                template = info_association.template
1229            # See if we have any field contents, but only if the info_association was
1230            # not inherited ( we do not want to display the inherited contents ).
1231            # (gvk: 8/30/10) Based on conversations with Dan, we agreed to ALWAYS inherit
1232            # contents.  We'll use this behavior until we hear from the community that
1233            # contents should not be inherited.  If we don't hear anything for a while,
1234            # eliminate the old commented out behavior.
1235            #if not inherited and get_contents:
1236            if get_contents:
1237                info = info_association.info
1238                if info:
1239                    return template.get_widgets( trans.user, info.content )
1240            else:
1241                return template.get_widgets( trans.user )
1242        return []
1243    def get_display_name( self ):
1244        """
1245        LibraryDatasetDatasetAssociation name can be either a string or a unicode object.
1246        If string, convert to unicode object assuming 'utf-8' format.
1247        """
1248        ldda_name = self.name
1249        if isinstance( ldda_name, str ):
1250            ldda_name = unicode( ldda_name, 'utf-8' )
1251        return ldda_name
1252
1253class LibraryInfoAssociation( object ):
1254    def __init__( self, library, form_definition, info, inheritable=False ):
1255        self.library = library
1256        self.template = form_definition
1257        self.info = info
1258        self.inheritable = inheritable
1259
1260class LibraryFolderInfoAssociation( object ):
1261    def __init__( self, folder, form_definition, info, inheritable=False ):
1262        self.folder = folder
1263        self.template = form_definition
1264        self.info = info
1265        self.inheritable = inheritable
1266
1267class LibraryDatasetDatasetInfoAssociation( object ):
1268    def __init__( self, library_dataset_dataset_association, form_definition, info ):
1269        # TODO: need to figure out if this should be inheritable to the associated LibraryDataset
1270        self.library_dataset_dataset_association = library_dataset_dataset_association
1271        self.template = form_definition
1272        self.info = info
1273
1274class ValidationError( object ):
1275    def __init__( self, message=None, err_type=None, attributes=None ):
1276        self.message = message
1277        self.err_type = err_type
1278        self.attributes = attributes
1279
1280class DatasetToValidationErrorAssociation( object ):
1281    def __init__( self, dataset, validation_error ):
1282        self.dataset = dataset
1283        self.validation_error = validation_error
1284
1285class ImplicitlyConvertedDatasetAssociation( object ):
1286    def __init__( self, id = None, parent = None, dataset = None, file_type = None, deleted = False, purged = False, metadata_safe = True ):
1287        self.id = id
1288        self.dataset = dataset
1289        self.parent = parent
1290        self.type = file_type
1291        self.deleted = deleted
1292        self.purged = purged
1293        self.metadata_safe = metadata_safe
1294
1295    def clear( self, purge = False ):
1296        self.deleted = True
1297        if self.dataset:
1298            self.dataset.deleted = True
1299            self.dataset.purged = purge
1300        if purge: #do something with purging
1301            self.purged = True
1302            try: os.unlink( self.file_name )
1303            except Exception, e: print "Failed to purge associated file (%s) from disk: %s" % ( self.file_name, e )
1304
1305class Event( object ):
1306    def __init__( self, message=None, history=None, user=None, galaxy_session=None ):
1307        self.history = history
1308        self.galaxy_session = galaxy_session
1309        self.user = user
1310        self.tool_id = None
1311        self.message = message
1312
1313class GalaxySession( object ):
1314    def __init__( self,
1315                  id=None,
1316                  user=None,
1317                  remote_host=None,
1318                  remote_addr=None,
1319                  referer=None,
1320                  current_history=None,
1321                  session_key=None,
1322                  is_valid=False,
1323                  prev_session_id=None ):
1324        self.id = id
1325        self.user = user
1326        self.remote_host = remote_host
1327        self.remote_addr = remote_addr
1328        self.referer = referer
1329        self.current_history = current_history
1330        self.session_key = session_key
1331        self.is_valid = is_valid
1332        self.prev_session_id = prev_session_id
1333        self.histories = []
1334    def add_history( self, history, association=None ):
1335        if association is None:
1336            self.histories.append( GalaxySessionToHistoryAssociation( self, history ) )
1337        else:
1338            self.histories.append( association )
1339   
1340class GalaxySessionToHistoryAssociation( object ):
1341    def __init__( self, galaxy_session, history ):
1342        self.galaxy_session = galaxy_session
1343        self.history = history
1344
1345class CloudImage( object ):
1346    def __init__( self ):
1347        self.id = None
1348        self.instance_id = None
1349        self.state = None
1350       
1351class UCI( object ):
1352    def __init__( self ):
1353        self.id = None
1354        self.user = None
1355
1356class CloudInstance( object ):
1357    def __init__( self ):
1358        self.id = None
1359        self.user = None
1360        self.name = None
1361        self.instance_id = None
1362        self.mi = None
1363        self.state = None
1364        self.public_dns = None
1365        self.availability_zone = None
1366       
1367class CloudStore( object ):
1368    def __init__( self ):
1369        self.id = None
1370        self.volume_id = None
1371        self.user = None
1372        self.size = None
1373        self.availability_zone = None
1374       
1375class CloudSnapshot( object ):
1376    def __init__( self ):
1377        self.id = None
1378        self.user = None
1379        self.store_id = None
1380        self.snapshot_id = None
1381       
1382class CloudProvider( object ):
1383    def __init__( self ):
1384        self.id = None
1385        self.user = None
1386        self.type = None
1387
1388class CloudUserCredentials( object ):
1389    def __init__( self ):
1390        self.id = None
1391        self.user = None
1392        self.name = None
1393        self.accessKey = None
1394        self.secretKey = None
1395        self.credentials = []
1396
1397class StoredWorkflow( object ):
1398    def __init__( self ):
1399        self.id = None
1400        self.user = None
1401        self.name = None
1402        self.slug = None
1403        self.published = False
1404        self.latest_workflow_id = None
1405        self.workflows = []
1406
1407class Workflow( object ):
1408    def __init__( self ):
1409        self.user = None
1410        self.name = None
1411        self.has_cycles = None
1412        self.has_errors = None
1413        self.steps = []
1414       
1415class WorkflowStep( object ):
1416    def __init__( self ):
1417        self.id = None
1418        self.type = None
1419        self.tool_id = None
1420        self.tool_inputs = None
1421        self.tool_errors = None
1422        self.position = None
1423        self.input_connections = []
1424        self.config = None
1425       
1426class WorkflowStepConnection( object ):
1427    def __init__( self ):
1428        self.output_step_id = None
1429        self.output_name = None
1430        self.input_step_id = None
1431        self.input_name = None
1432
1433class WorkflowOutput(object):
1434    def __init__( self, workflow_step, output_name):
1435        self.workflow_step = workflow_step
1436        self.output_name = output_name
1437       
1438class StoredWorkflowUserShareAssociation( object ):
1439    def __init__( self ):
1440        self.stored_workflow = None
1441        self.user = None
1442
1443class StoredWorkflowMenuEntry( object ):
1444    def __init__( self ):
1445        self.stored_workflow = None
1446        self.user = None
1447        self.order_index = None
1448
1449class WorkflowInvocation( object ):
1450    pass
1451
1452class WorkflowInvocationStep( object ):
1453    pass
1454
1455class MetadataFile( object ):
1456    def __init__( self, dataset = None, name = None ):
1457        if isinstance( dataset, HistoryDatasetAssociation ):
1458            self.history_dataset = dataset
1459        elif isinstance( dataset, LibraryDatasetDatasetAssociation ):
1460            self.library_dataset = dataset
1461        self.name = name
1462    @property
1463    def file_name( self ):
1464        assert self.id is not None, "ID must be set before filename used (commit the object)"
1465        path = os.path.join( Dataset.file_path, '_metadata_files', *directory_hash_id( self.id ) )
1466        # Create directory if it does not exist
1467        try:
1468            os.makedirs( path )
1469        except OSError, e:
1470            # File Exists is okay, otherwise reraise
1471            if e.errno != errno.EEXIST:
1472                raise
1473        # Return filename inside hashed directory
1474        return os.path.abspath( os.path.join( path, "metadata_%d.dat" % self.id ) )
1475
1476class FormDefinition( object ):
1477    types = Bunch(  REQUEST = 'Sequencing Request Form',
1478                    SAMPLE = 'Sequencing Sample Form',
1479                    LIBRARY_INFO_TEMPLATE = 'Library information template',
1480                    USER_INFO = 'User Information'  )
1481    def __init__(self, name=None, desc=None, fields=[],
1482                 form_definition_current=None, form_type=None, layout=None):
1483        self.name = name
1484        self.desc = desc
1485        self.fields = fields
1486        self.form_definition_current = form_definition_current
1487        self.type = form_type
1488        self.layout = layout
1489    def fields_of_grid(self, grid_index):
1490        '''
1491        This method returns the list of fields belonging to the given grid.
1492        '''
1493        gridfields = {}
1494        for i, f in enumerate(self.fields):
1495            if str(f['layout']) == str(grid_index):
1496                gridfields[i] = f
1497        return gridfields
1498    def get_widgets( self, user, contents=[], **kwd ):
1499        '''
1500        Return the list of widgets that comprise a form definition,
1501        including field contents if any.
1502        '''
1503        params = util.Params( kwd )
1504        widgets = []
1505        for index, field in enumerate( self.fields ):
1506            field_type = field[ 'type' ]
1507            field_name = 'field_%i' % index
1508            # determine the value of the field
1509            if field_name in kwd:
1510                # The form was submitted via refresh_on_change
1511                if field_type == 'CheckboxField':
1512                    value = CheckboxField.is_checked( params.get( field_name, False ) )
1513                else:
1514                    value = util.restore_text( params.get( field_name, '' ) )
1515            elif contents:
1516                try:
1517                    # This field has a saved value.
1518                    value = str( contents[ index ] )
1519                except:
1520                    # If there was an error getting the saved value, we'll still
1521                    # display the widget, but it will be empty.
1522                    if field_type == 'CheckboxField':
1523                        # Since we do not have contents, set checkbox value to False
1524                        value = False
1525                    else:
1526                        # Set other field types to empty string
1527                        value = ''
1528            else:
1529                # if none of the above, then leave the field empty
1530                if field_type == 'CheckboxField':
1531                    # Since we do not have contents, set checkbox value to False
1532                    value = False
1533                else:
1534                    # Set other field types to the default value of the field
1535                    value = field.get('default', '')
1536            # Create the field widget
1537            field_widget = eval( field_type )( field_name )
1538            if field_type == 'TextField':
1539                field_widget.set_size( 40 )
1540                field_widget.value = value
1541            elif field_type == 'TextArea':
1542                field_widget.set_size( 3, 40 )
1543                field_widget.value = value
1544            elif field_type == 'AddressField':
1545                field_widget.user = user
1546                field_widget.value = value
1547                field_widget.params = params
1548            elif field['type'] == 'WorkflowField':
1549                field_widget.user = user
1550                field_widget.value = value
1551                field_widget.params = params
1552            elif field_type == 'SelectField':
1553                for option in field[ 'selectlist' ]:
1554                    if option == value:
1555                        field_widget.add_option( option, option, selected=True )
1556                    else:
1557                        field_widget.add_option( option, option )
1558            elif field_type == 'CheckboxField':
1559                field_widget.set_checked( value )
1560            if field[ 'required' ] == 'required':
1561                req = 'Required'
1562            else:
1563                req = 'Optional'
1564            if field[ 'helptext' ]:
1565                helptext='%s (%s)' % ( field[ 'helptext' ], req )
1566            else:
1567                helptext = ''
1568            widgets.append( dict( label=field[ 'label' ],
1569                                  widget=field_widget,
1570                                  helptext=helptext ) )
1571        return widgets
1572       
1573class FormDefinitionCurrent( object ):
1574    def __init__(self, form_definition=None):
1575        self.latest_form = form_definition
1576       
1577class FormValues( object ):
1578    def __init__(self, form_def=None, content=None):
1579        self.form_definition = form_def
1580        self.content = content
1581       
1582class Request( object ):
1583    states = Bunch( NEW = 'New',
1584                    SUBMITTED = 'In Progress',
1585                    REJECTED = 'Rejected',
1586                    COMPLETE = 'Complete'   )
1587    def __init__( self, name=None, desc=None, request_type=None, user=None, form_values=None, notification=None ):
1588        self.name = name
1589        self.desc = desc
1590        self.type = request_type
1591        self.values = form_values
1592        self.user = user
1593        self.notification = notification
1594        self.samples_list = []
1595    @property
1596    def state( self ):
1597        latest_event = self.latest_event
1598        if latest_event:
1599            return latest_event.state
1600        return None
1601    @property
1602    def latest_event( self ):
1603        if self.events:
1604            return self.events[0]
1605        return None
1606    @property
1607    def samples_have_common_state( self ):
1608        """
1609        Returns the state of this request's samples when they are all
1610        in one common state. Otherwise returns False.
1611        """
1612        state_for_comparison = self.samples[0].state
1613        if state_for_comparison is None:
1614            for s in self.samples:
1615                if s.state is not None:
1616                    return False
1617        for s in self.samples:
1618            if s.state.id != state_for_comparison.id:
1619                return False
1620        return state_for_comparison
1621    @property
1622    def last_comment( self ):
1623        latest_event = self.latest_event
1624        if latest_event:
1625            if latest_event.comment:
1626                return latest_event.comment
1627            return ''
1628        return 'No comment'
1629    def has_sample( self, sample_name ):
1630        for s in self.samples:
1631            if s.name == sample_name:
1632                return s
1633        return False
1634    @property
1635    def is_unsubmitted( self ):
1636        return self.state in [ self.states.REJECTED, self.states.NEW ]
1637    @property
1638    def is_rejected( self ):
1639        return self.state == self.states.REJECTED
1640    @property
1641    def is_submitted( self ):
1642        return self.state == self.states.SUBMITTED
1643    @property
1644    def is_new( self ):
1645        return self.state == self.states.NEW
1646    @property
1647    def is_complete( self ):
1648        return self.state == self.states.COMPLETE
1649    @property
1650    def samples_without_library_destinations( self ):
1651        # Return all samples that are not associated with a library
1652        samples = []
1653        for sample in self.samples:
1654            if not sample.library:
1655                samples.append( sample )
1656        return samples
1657    def send_email_notification( self, trans, common_state, final_state=False ):
1658        # Check if an email notification is configured to be sent when the samples
1659        # are in this state
1660        if self.notification and common_state.id not in self.notification[ 'sample_states' ]:
1661            return
1662        comments = ''
1663        # Send email
1664        if trans.app.config.smtp_server is not None and self.notification and self.notification[ 'email' ]:
1665            host = trans.request.host.split( ':' )[0]
1666            if host in [ 'localhost', '127.0.0.1' ]:
1667                host = socket.getfqdn()
1668            body = """
1669Galaxy Sample Tracking Notification
1670===================================
1671
1672User:                     %(user)s
1673
1674Sequencing request:       %(request_name)s
1675Sequencer configuration:  %(request_type)s
1676Sequencing request state: %(request_state)s
1677
1678Number of samples:        %(num_samples)s
1679All samples in state:     %(sample_state)s
1680
1681"""
1682            values = dict( user=self.user.email,
1683                           request_name=self.name,
1684                           request_type=self.type.name,
1685                           request_state=self.state,
1686                           num_samples=str( len( self.samples ) ),
1687                           sample_state=common_state.name,
1688                           create_time=self.create_time,
1689                           submit_time=self.create_time )
1690            body = body % values
1691            # check if this is the final state of the samples
1692            if final_state:
1693                txt = "Sample Name -> Data Library/Folder\r\n"
1694                for s in self.samples:
1695                    txt = txt + "%s -> %s/%s\r\n" % ( s.name, s.library.name, s.folder.name )
1696                body = body + txt
1697            to = self.notification['email']
1698            frm = 'galaxy-no-reply@' + host
1699            subject = "Galaxy Sample Tracking notification: '%s' sequencing request" % self.name
1700            message = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s" % ( frm, ", ".join( to ), subject, body )
1701            try:
1702                s = smtplib.SMTP()
1703                s.connect( trans.app.config.smtp_server )
1704                s.sendmail( frm, to, message )
1705                s.quit()
1706                comments = "Email notification sent to %s." % ", ".join( to ).strip().strip( ',' )
1707            except:
1708                comments = "Email notification failed."
1709            # update the request history with the email notification event
1710        elif not trans.app.config.smtp_server:
1711            comments = "Email notification failed as SMTP server not set in config file"
1712        if comments:
1713            event = trans.app.model.RequestEvent( self, self.state, comments )
1714            trans.sa_session.add( event )
1715            trans.sa_session.flush()
1716        return comments
1717   
1718class RequestEvent( object ):
1719    def __init__(self, request=None, request_state=None, comment=''):
1720        self.request = request
1721        self.state = request_state
1722        self.comment = comment
1723       
1724class RequestType( object ):
1725    rename_dataset_options = Bunch( NO = 'Do not rename',
1726                                    SAMPLE_NAME = 'Preprend sample name',
1727                                    EXPERIMENT_NAME = 'Prepend experiment name',
1728                                    EXPERIMENT_AND_SAMPLE_NAME = 'Prepend experiment and sample name')
1729    permitted_actions = get_permitted_actions( filter='REQUEST_TYPE' )
1730    def __init__( self, name=None, desc=None, request_form=None, sample_form=None, datatx_info=None ):
1731        self.name = name
1732        self.desc = desc
1733        self.request_form = request_form
1734        self.sample_form = sample_form
1735        self.datatx_info = datatx_info
1736    @property
1737    def state( self ):
1738        # The states mapper for this object orders ascending
1739        return self.states[-1]
1740       
1741class RequestTypePermissions( object ):
1742    def __init__( self, action, request_type, role ):
1743        self.action = action
1744        self.request_type = request_type
1745        self.role = role
1746   
1747class Sample( object ):
1748    bulk_operations = Bunch( CHANGE_STATE = 'Change state',
1749                             SELECT_LIBRARY = 'Select data library and folder' )
1750    transfer_status = Bunch( NOT_STARTED = 'Not started',
1751                             IN_QUEUE = 'In queue',
1752                             TRANSFERRING = 'Transferring dataset',
1753                             ADD_TO_LIBRARY = 'Adding to data library',
1754                             COMPLETE = 'Complete',
1755                             ERROR = 'Error' )
1756    def __init__(self, name=None, desc=None, request=None, form_values=None, bar_code=None, library=None, folder=None):
1757        self.name = name
1758        self.desc = desc
1759        self.request = request
1760        self.values = form_values
1761        self.bar_code = bar_code
1762        self.library = library
1763        self.folder = folder
1764    @property
1765    def state( self ):
1766        latest_event = self.latest_event
1767        if latest_event:
1768            return latest_event.state
1769        return None
1770    @property
1771    def latest_event( self ):
1772        if self.events:
1773            return self.events[0]
1774        return None
1775    @property
1776    def untransferred_dataset_files( self ):
1777        untransferred_datasets = []
1778        for dataset in self.datasets:
1779            if dataset.status == self.transfer_status.NOT_STARTED:
1780                untransferred_datasets.append( dataset )
1781        return untransferred_datasets
1782    @property
1783    def inprogress_dataset_files( self ):
1784        inprogress_datasets = []
1785        for dataset in self.datasets:
1786            if dataset.status not in [ self.transfer_status.NOT_STARTED, self.transfer_status.COMPLETE ]:
1787                inprogress_datasets.append( dataset )
1788        return inprogress_datasets
1789    @property
1790    def transferred_dataset_files( self ):
1791        transferred_datasets = []
1792        for dataset in self.datasets:
1793            if dataset.status == self.transfer_status.COMPLETE:
1794                transferred_datasets.append( dataset )
1795        return transferred_datasets
1796    def dataset_size( self, filepath ):
1797        def print_ticks(d):
1798            pass
1799        datatx_info = self.request.type.datatx_info
1800        cmd  = 'ssh %s@%s "du -sh \'%s\'"' % ( datatx_info['username'],
1801                                          datatx_info['host'],
1802                                          filepath)
1803        output = pexpect.run(cmd, events={'.ssword:*': datatx_info['password']+'\r\n',
1804                                          pexpect.TIMEOUT:print_ticks},
1805                                          timeout=10)
1806        return output.replace(filepath, '').strip()
1807
1808class SampleState( object ):
1809    def __init__(self, name=None, desc=None, request_type=None):
1810        self.name = name
1811        self.desc = desc
1812        self.request_type = request_type
1813
1814class SampleEvent( object ):
1815    def __init__(self, sample=None, sample_state=None, comment=''):
1816        self.sample = sample
1817        self.state = sample_state
1818        self.comment = comment
1819       
1820class SampleDataset( object ):
1821    def __init__(self, sample=None, name=None, file_path=None,
1822                 status=None, error_msg=None, size=None):
1823        self.sample = sample
1824        self.name = name
1825        self.file_path = file_path
1826        self.status = status
1827        self.error_msg = error_msg
1828        self.size = size
1829       
1830class UserAddress( object ):
1831    def __init__(self, user=None, desc=None, name=None, institution=None,
1832                 address=None, city=None, state=None, postal_code=None,
1833                 country=None, phone=None):
1834        self.user = user
1835        self.desc = desc
1836        self.name = name
1837        self.institution = institution
1838        self.address = address
1839        self.city = city
1840        self.state = state
1841        self.postal_code = postal_code
1842        self.country = country
1843        self.phone = phone
1844    def get_html(self):
1845        html = ''
1846        if self.name:
1847            html = html + self.name
1848        if self.institution:
1849            html = html + '<br/>' + self.institution
1850        if self.address:
1851            html = html + '<br/>' + self.address
1852        if self.city:
1853            html = html + '<br/>' + self.city
1854        if self.state:
1855            html = html + ' ' + self.state
1856        if self.postal_code:
1857            html = html + ' ' + self.postal_code
1858        if self.country:
1859            html = html + '<br/>' + self.country
1860        if self.phone:
1861            html = html + '<br/>' + 'Phone: ' + self.phone
1862        return html
1863
1864class Page( object ):
1865    def __init__( self ):
1866        self.id = None
1867        self.user = None
1868        self.title = None
1869        self.slug = None
1870        self.latest_revision_id = None
1871        self.revisions = []
1872        self.importable = None
1873        self.published = None
1874
1875class PageRevision( object ):
1876    def __init__( self ):
1877        self.user = None
1878        self.title = None
1879        self.content = None
1880       
1881class PageUserShareAssociation( object ):
1882    def __init__( self ):
1883        self.page = None
1884        self.user = None
1885
1886class Visualization( object ):
1887    def __init__( self ):
1888        self.id = None
1889        self.user = None
1890        self.type = None
1891        self.title = None
1892        self.latest_revision = None
1893        self.revisions = []
1894
1895class VisualizationRevision( object ):
1896    def __init__( self ):
1897        self.id = None
1898        self.visualization = None
1899        self.title = None
1900        self.config = None
1901       
1902class VisualizationUserShareAssociation( object ):
1903    def __init__( self ):
1904        self.visualization = None
1905        self.user = None
1906       
1907class Tag ( object ):
1908    def __init__( self, id=None, type=None, parent_id=None, name=None ):
1909        self.id = id
1910        self.type = type
1911        self.parent_id = parent_id
1912        self.name = name
1913       
1914    def __str__ ( self ):
1915        return "Tag(id=%s, type=%i, parent_id=%s, name=%s)" %  ( self.id, self.type, self.parent_id, self.name )
1916   
1917class ItemTagAssociation ( object ):
1918    def __init__( self, id=None, user=None, item_id=None, tag_id=None, user_tname=None, value=None ):
1919        self.id = id
1920        self.user = user
1921        self.item_id = item_id
1922        self.tag_id = tag_id
1923        self.user_tname = user_tname
1924        self.value = None
1925        self.user_value = None
1926       
1927class HistoryTagAssociation ( ItemTagAssociation ):
1928    pass
1929   
1930class DatasetTagAssociation ( ItemTagAssociation ):
1931    pass
1932   
1933class HistoryDatasetAssociationTagAssociation ( ItemTagAssociation ):
1934    pass
1935
1936class PageTagAssociation ( ItemTagAssociation ):
1937    pass
1938
1939class WorkflowStepTagAssociation ( ItemTagAssociation ):
1940    pass
1941   
1942class StoredWorkflowTagAssociation ( ItemTagAssociation ):
1943    pass
1944   
1945class VisualizationTagAssociation ( ItemTagAssociation ):
1946    pass
1947   
1948# Item annotation classes.
1949   
1950class HistoryAnnotationAssociation( object ):
1951    pass
1952   
1953class HistoryDatasetAssociationAnnotationAssociation( object ):
1954    pass
1955   
1956class StoredWorkflowAnnotationAssociation( object ):
1957    pass
1958   
1959class WorkflowStepAnnotationAssociation( object ):
1960    pass
1961   
1962class PageAnnotationAssociation( object ):
1963    pass
1964   
1965class VisualizationAnnotationAssociation( object ):
1966    pass
1967   
1968# Item rating classes.
1969   
1970class ItemRatingAssociation( object ):
1971    def __init__( self, id=None, user=None, item=None, rating=0 ):
1972        self.id = id
1973        self.user = user
1974        self.item = item
1975        self.rating = rating
1976       
1977    def set_item( self, item ):
1978        """ Set association's item. """
1979        pass
1980       
1981class HistoryRatingAssociation( ItemRatingAssociation ):
1982    def set_item( self, history ):
1983        self.history = history
1984   
1985class HistoryDatasetAssociationRatingAssociation( ItemRatingAssociation ):
1986    def set_item( self, history_dataset_association ):
1987        self.history_dataset_association = history_dataset_association
1988   
1989class StoredWorkflowRatingAssociation( ItemRatingAssociation ):
1990    def set_item( self, stored_workflow ):
1991        self.stored_workflow = stored_workflow
1992   
1993class PageRatingAssociation( ItemRatingAssociation ):
1994    def set_item( self, page ):
1995        self.page = page
1996
1997class VisualizationRatingAssociation( ItemRatingAssociation ):
1998    def set_item( self, visualization ):
1999        self.visualization = visualization
2000   
2001class UserPreference ( object ):
2002    def __init__( self, name=None, value=None ):
2003        self.name = name
2004        self.value = value
2005       
2006class UserAction( object ):
2007    def __init__( self, id=None, create_time=None, user_id=None, session_id=None, action=None, params=None, context=None):
2008        self.id = id
2009        self.create_time = create_time
2010        self.user_id = user_id
2011        self.session_id = session_id
2012        self.action = action
2013        self.params = params
2014        self.context = context
2015
2016class APIKeys( object ):
2017    pass
2018
2019## ---- Utility methods -------------------------------------------------------
2020
2021def directory_hash_id( id ):
2022    s = str( id )
2023    l = len( s )
2024    # Shortcut -- ids 0-999 go under ../000/
2025    if l < 4:
2026        return [ "000" ]
2027    # Pad with zeros until a multiple of three
2028    padded = ( ( 3 - len( s ) % 3 ) * "0" ) + s
2029    # Drop the last three digits -- 1000 files per directory
2030    padded = padded[:-3]
2031    # Break into chunks of three
2032    return [ padded[i*3:(i+1)*3] for i in range( len( padded ) // 3 ) ]
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。