1 | import re, logging |
---|
2 | from sqlalchemy.sql.expression import func, and_ |
---|
3 | from sqlalchemy.sql import select |
---|
4 | |
---|
5 | log = logging.getLogger( __name__ ) |
---|
6 | |
---|
7 | # Item-specific information needed to perform tagging. |
---|
8 | class ItemTagAssocInfo( object ): |
---|
9 | def __init__( self, item_class, tag_assoc_class, item_id_col ): |
---|
10 | self.item_class = item_class |
---|
11 | self.tag_assoc_class = tag_assoc_class |
---|
12 | self.item_id_col = item_id_col |
---|
13 | |
---|
14 | class TagHandler( object ): |
---|
15 | def __init__( self ): |
---|
16 | # Minimum tag length. |
---|
17 | self.min_tag_len = 2 |
---|
18 | # Maximum tag length. |
---|
19 | self.max_tag_len = 255 |
---|
20 | # Tag separator. |
---|
21 | self.tag_separators = ',;' |
---|
22 | # Hierarchy separator. |
---|
23 | self.hierarchy_separator = '.' |
---|
24 | # Key-value separator. |
---|
25 | self.key_value_separators = "=:" |
---|
26 | # Initialize with known classes - add to this in subclasses. |
---|
27 | self.item_tag_assoc_info = {} |
---|
28 | def get_tag_assoc_class( self, item_class ): |
---|
29 | """Returns tag association class for item class.""" |
---|
30 | return self.item_tag_assoc_info[item_class.__name__].tag_assoc_class |
---|
31 | def get_id_col_in_item_tag_assoc_table( self, item_class ): |
---|
32 | """Returns item id column in class' item-tag association table.""" |
---|
33 | return self.item_tag_assoc_info[item_class.__name__].item_id_col |
---|
34 | def get_community_tags( self, trans, item=None, limit=None ): |
---|
35 | """Returns community tags for an item.""" |
---|
36 | # Get item-tag association class. |
---|
37 | item_class = item.__class__ |
---|
38 | item_tag_assoc_class = self.get_tag_assoc_class( item_class ) |
---|
39 | if not item_tag_assoc_class: |
---|
40 | return [] |
---|
41 | # Build select statement. |
---|
42 | cols_to_select = [ item_tag_assoc_class.table.c.tag_id, func.count( '*' ) ] |
---|
43 | from_obj = item_tag_assoc_class.table.join( item_class.table ).join( trans.app.model.Tag.table ) |
---|
44 | where_clause = ( self.get_id_col_in_item_tag_assoc_table( item_class ) == item.id ) |
---|
45 | order_by = [ func.count( "*" ).desc() ] |
---|
46 | group_by = item_tag_assoc_class.table.c.tag_id |
---|
47 | # Do query and get result set. |
---|
48 | query = select( columns=cols_to_select, |
---|
49 | from_obj=from_obj, |
---|
50 | whereclause=where_clause, |
---|
51 | group_by=group_by, |
---|
52 | order_by=order_by, |
---|
53 | limit=limit ) |
---|
54 | result_set = trans.sa_session.execute( query ) |
---|
55 | # Return community tags. |
---|
56 | community_tags = [] |
---|
57 | for row in result_set: |
---|
58 | tag_id = row[0] |
---|
59 | community_tags.append( self.get_tag_by_id( trans, tag_id ) ) |
---|
60 | return community_tags |
---|
61 | def remove_item_tag( self, trans, user, item, tag_name ): |
---|
62 | """Remove a tag from an item.""" |
---|
63 | # Get item tag association. |
---|
64 | item_tag_assoc = self._get_item_tag_assoc( user, item, tag_name ) |
---|
65 | # Remove association. |
---|
66 | if item_tag_assoc: |
---|
67 | # Delete association. |
---|
68 | trans.sa_session.delete( item_tag_assoc ) |
---|
69 | item.tags.remove( item_tag_assoc ) |
---|
70 | return True |
---|
71 | return False |
---|
72 | def delete_item_tags( self, trans, user, item ): |
---|
73 | """Delete tags from an item.""" |
---|
74 | # Delete item-tag associations. |
---|
75 | for tag in item.tags: |
---|
76 | trans.sa_session.delete( tag ) |
---|
77 | # Delete tags from item. |
---|
78 | del item.tags[:] |
---|
79 | def item_has_tag( self, trans, user, item, tag ): |
---|
80 | """Returns true if item is has a given tag.""" |
---|
81 | # Get tag name. |
---|
82 | if isinstance( tag, basestring ): |
---|
83 | tag_name = tag |
---|
84 | elif isinstance( tag, trans.app.model.Tag ): |
---|
85 | tag_name = tag.name |
---|
86 | # Check for an item-tag association to see if item has a given tag. |
---|
87 | item_tag_assoc = self._get_item_tag_assoc( user, item, tag_name ) |
---|
88 | if item_tag_assoc: |
---|
89 | return True |
---|
90 | return False |
---|
91 | def apply_item_tags( self, trans, user, item, tags_str ): |
---|
92 | """Apply tags to an item.""" |
---|
93 | # Parse tags. |
---|
94 | parsed_tags = self.parse_tags( tags_str ) |
---|
95 | # Apply each tag. |
---|
96 | for name, value in parsed_tags.items(): |
---|
97 | # Use lowercase name for searching/creating tag. |
---|
98 | lc_name = name.lower() |
---|
99 | # Get or create item-tag association. |
---|
100 | item_tag_assoc = self._get_item_tag_assoc( user, item, lc_name ) |
---|
101 | if not item_tag_assoc: |
---|
102 | # Create item-tag association. |
---|
103 | # Create tag; if None, skip the tag (and log error). |
---|
104 | tag = self._get_or_create_tag( trans, lc_name ) |
---|
105 | if not tag: |
---|
106 | # Log error? |
---|
107 | continue |
---|
108 | # Create tag association based on item class. |
---|
109 | item_tag_assoc_class = self.get_tag_assoc_class( item.__class__ ) |
---|
110 | item_tag_assoc = item_tag_assoc_class() |
---|
111 | # Add tag to association. |
---|
112 | item.tags.append( item_tag_assoc ) |
---|
113 | item_tag_assoc.tag = tag |
---|
114 | item_tag_assoc.user = user |
---|
115 | # Apply attributes to item-tag association. Strip whitespace from user name and tag. |
---|
116 | lc_value = None |
---|
117 | if value: |
---|
118 | lc_value = value.lower() |
---|
119 | item_tag_assoc.user_tname = name |
---|
120 | item_tag_assoc.user_value = value |
---|
121 | item_tag_assoc.value = lc_value |
---|
122 | def get_tags_str( self, tags ): |
---|
123 | """Build a string from an item's tags.""" |
---|
124 | # Return empty string if there are no tags. |
---|
125 | if not tags: |
---|
126 | return "" |
---|
127 | # Create string of tags. |
---|
128 | tags_str_list = list() |
---|
129 | for tag in tags: |
---|
130 | tag_str = tag.user_tname |
---|
131 | if tag.value is not None: |
---|
132 | tag_str += ":" + tag.user_value |
---|
133 | tags_str_list.append( tag_str ) |
---|
134 | return ", ".join( tags_str_list ) |
---|
135 | def get_tag_by_id( self, trans, tag_id ): |
---|
136 | """Get a Tag object from a tag id.""" |
---|
137 | return trans.sa_session.query( trans.app.model.Tag ).filter_by( id=tag_id ).first() |
---|
138 | def get_tag_by_name( self, trans, tag_name ): |
---|
139 | """Get a Tag object from a tag name (string).""" |
---|
140 | if tag_name: |
---|
141 | return trans.sa_session.query( trans.app.model.Tag ).filter_by( name=tag_name.lower() ).first() |
---|
142 | return None |
---|
143 | def _create_tag( self, trans, tag_str ): |
---|
144 | """Create a Tag object from a tag string.""" |
---|
145 | tag_hierarchy = tag_str.split( self.hierarchy_separator ) |
---|
146 | tag_prefix = "" |
---|
147 | parent_tag = None |
---|
148 | for sub_tag in tag_hierarchy: |
---|
149 | # Get or create subtag. |
---|
150 | tag_name = tag_prefix + self._scrub_tag_name( sub_tag ) |
---|
151 | tag = trans.sa_session.query( trans.app.model.Tag ).filter_by( name=tag_name).first() |
---|
152 | if not tag: |
---|
153 | tag = trans.app.model.Tag( type=0, name=tag_name ) |
---|
154 | # Set tag parent. |
---|
155 | tag.parent = parent_tag |
---|
156 | # Update parent and tag prefix. |
---|
157 | parent_tag = tag |
---|
158 | tag_prefix = tag.name + self.hierarchy_separator |
---|
159 | return tag |
---|
160 | def _get_or_create_tag( self, trans, tag_str ): |
---|
161 | """Get or create a Tag object from a tag string.""" |
---|
162 | # Scrub tag; if tag is None after being scrubbed, return None. |
---|
163 | scrubbed_tag_str = self._scrub_tag_name( tag_str ) |
---|
164 | if not scrubbed_tag_str: |
---|
165 | return None |
---|
166 | # Get item tag. |
---|
167 | tag = self.get_tag_by_name( trans, scrubbed_tag_str ) |
---|
168 | # Create tag if necessary. |
---|
169 | if tag is None: |
---|
170 | tag = self._create_tag( trans, scrubbed_tag_str ) |
---|
171 | return tag |
---|
172 | def _get_item_tag_assoc( self, user, item, tag_name ): |
---|
173 | """ |
---|
174 | Return ItemTagAssociation object for a user, item, and tag string; returns None if there is |
---|
175 | no such association. |
---|
176 | """ |
---|
177 | scrubbed_tag_name = self._scrub_tag_name( tag_name ) |
---|
178 | for item_tag_assoc in item.tags: |
---|
179 | if ( item_tag_assoc.user == user ) and ( item_tag_assoc.user_tname == scrubbed_tag_name ): |
---|
180 | return item_tag_assoc |
---|
181 | return None |
---|
182 | def parse_tags( self, tag_str ): |
---|
183 | """ |
---|
184 | Returns a list of raw (tag-name, value) pairs derived from a string; method scrubs tag names and values as well. |
---|
185 | Return value is a dictionary where tag-names are keys. |
---|
186 | """ |
---|
187 | # Gracefully handle None. |
---|
188 | if not tag_str: |
---|
189 | return dict() |
---|
190 | # Split tags based on separators. |
---|
191 | reg_exp = re.compile( '[' + self.tag_separators + ']' ) |
---|
192 | raw_tags = reg_exp.split( tag_str ) |
---|
193 | # Extract name-value pairs. |
---|
194 | name_value_pairs = dict() |
---|
195 | for raw_tag in raw_tags: |
---|
196 | nv_pair = self._get_name_value_pair( raw_tag ) |
---|
197 | scrubbed_name = self._scrub_tag_name( nv_pair[0] ) |
---|
198 | scrubbed_value = self._scrub_tag_value( nv_pair[1] ) |
---|
199 | name_value_pairs[scrubbed_name] = scrubbed_value |
---|
200 | return name_value_pairs |
---|
201 | def _scrub_tag_value( self, value ): |
---|
202 | """Scrub a tag value.""" |
---|
203 | # Gracefully handle None: |
---|
204 | if not value: |
---|
205 | return None |
---|
206 | # Remove whitespace from value. |
---|
207 | reg_exp = re.compile( '\s' ) |
---|
208 | scrubbed_value = re.sub( reg_exp, "", value ) |
---|
209 | return scrubbed_value |
---|
210 | def _scrub_tag_name( self, name ): |
---|
211 | """Scrub a tag name.""" |
---|
212 | # Gracefully handle None: |
---|
213 | if not name: |
---|
214 | return None |
---|
215 | # Remove whitespace from name. |
---|
216 | reg_exp = re.compile( '\s' ) |
---|
217 | scrubbed_name = re.sub( reg_exp, "", name ) |
---|
218 | # Ignore starting ':' char. |
---|
219 | if scrubbed_name.startswith( self.hierarchy_separator ): |
---|
220 | scrubbed_name = scrubbed_name[1:] |
---|
221 | # If name is too short or too long, return None. |
---|
222 | if len( scrubbed_name ) < self.min_tag_len or len( scrubbed_name ) > self.max_tag_len: |
---|
223 | return None |
---|
224 | return scrubbed_name |
---|
225 | def _scrub_tag_name_list( self, tag_name_list ): |
---|
226 | """Scrub a tag name list.""" |
---|
227 | scrubbed_tag_list = list() |
---|
228 | for tag in tag_name_list: |
---|
229 | scrubbed_tag_list.append( self._scrub_tag_name( tag ) ) |
---|
230 | return scrubbed_tag_list |
---|
231 | def _get_name_value_pair( self, tag_str ): |
---|
232 | """Get name, value pair from a tag string.""" |
---|
233 | # Use regular expression to parse name, value. |
---|
234 | reg_exp = re.compile( "[" + self.key_value_separators + "]" ) |
---|
235 | name_value_pair = reg_exp.split( tag_str ) |
---|
236 | # Add empty slot if tag does not have value. |
---|
237 | if len( name_value_pair ) < 2: |
---|
238 | name_value_pair.append( None ) |
---|
239 | return name_value_pair |
---|
240 | |
---|
241 | class GalaxyTagHandler( TagHandler ): |
---|
242 | def __init__( self ): |
---|
243 | from galaxy import model |
---|
244 | TagHandler.__init__( self ) |
---|
245 | self.item_tag_assoc_info["History"] = ItemTagAssocInfo( model.History, |
---|
246 | model.HistoryTagAssociation, |
---|
247 | model.HistoryTagAssociation.table.c.history_id ) |
---|
248 | self.item_tag_assoc_info["HistoryDatasetAssociation"] = \ |
---|
249 | ItemTagAssocInfo( model.HistoryDatasetAssociation, |
---|
250 | model.HistoryDatasetAssociationTagAssociation, |
---|
251 | model.HistoryDatasetAssociationTagAssociation.table.c.history_dataset_association_id ) |
---|
252 | self.item_tag_assoc_info["Page"] = ItemTagAssocInfo( model.Page, |
---|
253 | model.PageTagAssociation, |
---|
254 | model.PageTagAssociation.table.c.page_id ) |
---|
255 | self.item_tag_assoc_info["StoredWorkflow"] = ItemTagAssocInfo( model.StoredWorkflow, |
---|
256 | model.StoredWorkflowTagAssociation, |
---|
257 | model.StoredWorkflowTagAssociation.table.c.stored_workflow_id ) |
---|
258 | self.item_tag_assoc_info["Visualization"] = ItemTagAssocInfo( model.Visualization, |
---|
259 | model.VisualizationTagAssociation, |
---|
260 | model.VisualizationTagAssociation.table.c.visualization_id ) |
---|
261 | |
---|
262 | class CommunityTagHandler( TagHandler ): |
---|
263 | def __init__( self ): |
---|
264 | from galaxy.webapps.community import model |
---|
265 | TagHandler.__init__( self ) |
---|