|
a/Allura/allura/model/artifact.py |
|
b/Allura/allura/model/artifact.py |
|
... |
|
... |
214 |
for l in links:
|
214 |
for l in links:
|
215 |
if app_id == l.tool_name: return l
|
215 |
if app_id == l.tool_name: return l
|
216 |
return None
|
216 |
return None
|
217 |
|
217 |
|
218 |
class Feed(MappedClass):
|
218 |
class Feed(MappedClass):
|
|
|
219 |
"""
|
|
|
220 |
Used to generate rss/atom feeds. This does not need to be extended;
|
|
|
221 |
all feed items go into the same collection
|
|
|
222 |
"""
|
219 |
class __mongometa__:
|
223 |
class __mongometa__:
|
220 |
session = project_orm_session
|
224 |
session = project_orm_session
|
221 |
name = 'artifact_feed'
|
225 |
name = 'artifact_feed'
|
222 |
indexes = [
|
226 |
indexes = [
|
223 |
'pubdate',
|
227 |
'pubdate',
|
|
... |
|
... |
233 |
author_name = FieldProperty(str, if_missing=lambda:c.user.get_pref('display_name') if hasattr(c, 'user') else None)
|
237 |
author_name = FieldProperty(str, if_missing=lambda:c.user.get_pref('display_name') if hasattr(c, 'user') else None)
|
234 |
author_link = FieldProperty(str, if_missing=lambda:c.user.url() if hasattr(c, 'user') else None)
|
238 |
author_link = FieldProperty(str, if_missing=lambda:c.user.url() if hasattr(c, 'user') else None)
|
235 |
|
239 |
|
236 |
@classmethod
|
240 |
@classmethod
|
237 |
def post(cls, artifact, title=None, description=None):
|
241 |
def post(cls, artifact, title=None, description=None):
|
|
|
242 |
"Create a Feed item"
|
238 |
idx = artifact.index()
|
243 |
idx = artifact.index()
|
239 |
if title is None:
|
244 |
if title is None:
|
240 |
title='%s modified by %s' % (idx['title_s'], c.user.get_pref('display_name'))
|
245 |
title='%s modified by %s' % (idx['title_s'], c.user.get_pref('display_name'))
|
241 |
if description is None: description = title
|
246 |
if description is None: description = title
|
242 |
item = cls(artifact_reference=artifact.dump_ref(),
|
247 |
item = cls(artifact_reference=artifact.dump_ref(),
|
|
... |
|
... |
246 |
return item
|
251 |
return item
|
247 |
|
252 |
|
248 |
@classmethod
|
253 |
@classmethod
|
249 |
def feed(cls, q, feed_type, title, link, description,
|
254 |
def feed(cls, q, feed_type, title, link, description,
|
250 |
since=None, until=None, offset=None, limit=None):
|
255 |
since=None, until=None, offset=None, limit=None):
|
|
|
256 |
"Produces webhelper.feedgenerator Feed"
|
251 |
d = dict(title=title, link=h.absurl(link), description=description, language=u'en')
|
257 |
d = dict(title=title, link=h.absurl(link), description=description, language=u'en')
|
252 |
if feed_type == 'atom':
|
258 |
if feed_type == 'atom':
|
253 |
feed = FG.Atom1Feed(**d)
|
259 |
feed = FG.Atom1Feed(**d)
|
254 |
elif feed_type == 'rss':
|
260 |
elif feed_type == 'rss':
|
255 |
feed = FG.Rss201rev2Feed(**d)
|
261 |
feed = FG.Rss201rev2Feed(**d)
|
|
... |
|
... |
273 |
author_name=r.author_name,
|
279 |
author_name=r.author_name,
|
274 |
author_link=h.absurl(r.author_link))
|
280 |
author_link=h.absurl(r.author_link))
|
275 |
return feed
|
281 |
return feed
|
276 |
|
282 |
|
277 |
class Artifact(MappedClass):
|
283 |
class Artifact(MappedClass):
|
|
|
284 |
"""
|
|
|
285 |
The base class for anything you want to keep track of.
|
|
|
286 |
|
|
|
287 |
It will automatically be added to solr (see index() method). It also
|
|
|
288 |
gains a discussion thread and can have files attached to it.
|
|
|
289 |
|
|
|
290 |
:var tool_version: default's to the app's version
|
|
|
291 |
:var acl: dict of permission name => [roles]
|
|
|
292 |
:var labels: list of plain old strings
|
|
|
293 |
:var references: list of outgoing references to other tickets
|
|
|
294 |
:var backreferences: dict of incoming references to this artifact, mapped by solr id
|
|
|
295 |
"""
|
|
|
296 |
|
278 |
class __mongometa__:
|
297 |
class __mongometa__:
|
279 |
session = artifact_orm_session
|
298 |
session = artifact_orm_session
|
280 |
name='artifact'
|
299 |
name='artifact'
|
281 |
indexes = [ 'app_config_id' ]
|
300 |
indexes = [ 'app_config_id' ]
|
282 |
def before_save(data):
|
301 |
def before_save(data):
|
|
... |
|
... |
299 |
if_missing=lambda:{c.app.config.tool_name:c.app.__version__})
|
318 |
if_missing=lambda:{c.app.config.tool_name:c.app.__version__})
|
300 |
acl = FieldProperty({str:[S.ObjectId]})
|
319 |
acl = FieldProperty({str:[S.ObjectId]})
|
301 |
tags = FieldProperty([dict(tag=str, count=int)])
|
320 |
tags = FieldProperty([dict(tag=str, count=int)])
|
302 |
labels = FieldProperty([str])
|
321 |
labels = FieldProperty([str])
|
303 |
references = FieldProperty([ArtifactReferenceType])
|
322 |
references = FieldProperty([ArtifactReferenceType])
|
304 |
backreferences = FieldProperty({str:ArtifactReferenceType})
|
323 |
backreferences = FieldProperty({str:ArtifactReferenceType}) # keyed by solr id to emulate a set
|
305 |
app_config = RelationProperty('AppConfig')
|
324 |
app_config = RelationProperty('AppConfig')
|
306 |
|
325 |
|
307 |
@classmethod
|
326 |
@classmethod
|
308 |
def attachment_class(cls):
|
327 |
def attachment_class(cls):
|
309 |
raise NotImplementedError, 'attachment_class'
|
328 |
raise NotImplementedError, 'attachment_class'
|
|
... |
|
... |
429 |
l = self.acl.setdefault(at, [])
|
448 |
l = self.acl.setdefault(at, [])
|
430 |
if project_role_id in l:
|
449 |
if project_role_id in l:
|
431 |
l.remove(project_role_id)
|
450 |
l.remove(project_role_id)
|
432 |
|
451 |
|
433 |
def index_id(self):
|
452 |
def index_id(self):
|
|
|
453 |
"Used for solr search indexing"
|
434 |
id = '%s.%s#%s' % (
|
454 |
id = '%s.%s#%s' % (
|
435 |
self.__class__.__module__,
|
455 |
self.__class__.__module__,
|
436 |
self.__class__.__name__,
|
456 |
self.__class__.__name__,
|
437 |
self._id)
|
457 |
self._id)
|
438 |
return id.replace('.', '/')
|
458 |
return id.replace('.', '/')
|
439 |
|
459 |
|
440 |
def index(self):
|
460 |
def index(self):
|
|
|
461 |
"""
|
|
|
462 |
Subclasses should override this, providing a dictionary of solr_field => value.
|
|
|
463 |
These fields & values will be stored by solr. Subclasses should call the
|
|
|
464 |
super() index() and then extend it with more fields. All these fields will be
|
|
|
465 |
included in the 'text' field (done by search.solarize())
|
|
|
466 |
|
|
|
467 |
The _s and _t suffixes, for example, follow solr dynamic field naming pattern.
|
|
|
468 |
"""
|
|
|
469 |
|
441 |
project = self.project
|
470 |
project = self.project
|
442 |
return dict(
|
471 |
return dict(
|
443 |
id=self.index_id(),
|
472 |
id=self.index_id(),
|
444 |
mod_date_dt=self.mod_date,
|
473 |
mod_date_dt=self.mod_date,
|
445 |
title_s='Artifact %s' % self._id,
|
474 |
title_s='Artifact %s' % self._id,
|
|
... |
|
... |
454 |
tags_t=' '.join(t['tag'] for t in self.tags),
|
483 |
tags_t=' '.join(t['tag'] for t in self.tags),
|
455 |
labels_t=' '.join(l for l in self.labels),
|
484 |
labels_t=' '.join(l for l in self.labels),
|
456 |
snippet_s='')
|
485 |
snippet_s='')
|
457 |
|
486 |
|
458 |
def url(self):
|
487 |
def url(self):
|
|
|
488 |
"""
|
|
|
489 |
Subclasses should implement this, providing the URL to the artifact
|
|
|
490 |
"""
|
459 |
raise NotImplementedError, 'url' # pragma no cover
|
491 |
raise NotImplementedError, 'url' # pragma no cover
|
460 |
|
492 |
|
461 |
def shorthand_id(self):
|
493 |
def shorthand_id(self):
|
462 |
'''How to refer to this artifact within the app instance context.
|
494 |
'''How to refer to this artifact within the app instance context.
|
463 |
|
495 |
|
|
... |
|
... |
482 |
filename=filename,
|
514 |
filename=filename,
|
483 |
fp=fp, artifact_id=self._id, **kw)
|
515 |
fp=fp, artifact_id=self._id, **kw)
|
484 |
return att
|
516 |
return att
|
485 |
|
517 |
|
486 |
class Snapshot(Artifact):
|
518 |
class Snapshot(Artifact):
|
|
|
519 |
"""A snapshot of an :class:`Artifact <allura.model.Artifact>`, used in :class:`VersionedArtifact <allura.model.VersionedArtifact>`"""
|
487 |
class __mongometa__:
|
520 |
class __mongometa__:
|
488 |
session = artifact_orm_session
|
521 |
session = artifact_orm_session
|
489 |
name='artifact_snapshot'
|
522 |
name='artifact_snapshot'
|
490 |
unique_indexes = [ ('artifact_class', 'artifact_id', 'version') ]
|
523 |
unique_indexes = [ ('artifact_class', 'artifact_id', 'version') ]
|
491 |
indexes = [ ('artifact_id', 'version') ]
|
524 |
indexes = [ ('artifact_id', 'version') ]
|
|
... |
|
... |
522 |
|
555 |
|
523 |
def __getattr__(self, name):
|
556 |
def __getattr__(self, name):
|
524 |
return getattr(self.data, name)
|
557 |
return getattr(self.data, name)
|
525 |
|
558 |
|
526 |
class VersionedArtifact(Artifact):
|
559 |
class VersionedArtifact(Artifact):
|
|
|
560 |
"""
|
|
|
561 |
An :class:`Artifact <allura.model.Artifact>` that has versions.
|
|
|
562 |
Associated data like attachments and discussion thread are not versioned.
|
|
|
563 |
"""
|
527 |
class __mongometa__:
|
564 |
class __mongometa__:
|
528 |
session = artifact_orm_session
|
565 |
session = artifact_orm_session
|
529 |
name='versioned_artifact'
|
566 |
name='versioned_artifact'
|
530 |
history_class = Snapshot
|
567 |
history_class = Snapshot
|
531 |
|
568 |
|
|
... |
|
... |
592 |
return self.history().first().timestamp
|
629 |
return self.history().first().timestamp
|
593 |
else:
|
630 |
else:
|
594 |
return self.mod_date
|
631 |
return self.mod_date
|
595 |
|
632 |
|
596 |
class Message(Artifact):
|
633 |
class Message(Artifact):
|
|
|
634 |
"""
|
|
|
635 |
A message
|
|
|
636 |
|
|
|
637 |
:var _id: an email friendly (e.g. message-id) string id
|
|
|
638 |
:var slug: slash-delimeted random identifier. Slashes useful for threaded searching and ordering
|
|
|
639 |
:var full_slug: string of slash-delimited "timestamp:slug" components. Useful for sorting by timstamp
|
|
|
640 |
"""
|
|
|
641 |
|
597 |
class __mongometa__:
|
642 |
class __mongometa__:
|
598 |
session = artifact_orm_session
|
643 |
session = artifact_orm_session
|
599 |
name='message'
|
644 |
name='message'
|
600 |
indexes = Artifact.__mongometa__.indexes + [ 'slug', 'parent_id', 'timestamp' ]
|
645 |
indexes = Artifact.__mongometa__.indexes + [ 'slug', 'parent_id', 'timestamp' ]
|
601 |
type_s='Generic Message'
|
646 |
type_s='Generic Message'
|
|
... |
|
... |
709 |
|
754 |
|
710 |
def shorthand_id(self):
|
755 |
def shorthand_id(self):
|
711 |
return self.short
|
756 |
return self.short
|
712 |
|
757 |
|
713 |
class AwardGrant(Artifact):
|
758 |
class AwardGrant(Artifact):
|
|
|
759 |
"An :class:`Award <allura.model.Award>` can be bestowed upon a project by a neighborhood"
|
714 |
class __mongometa__:
|
760 |
class __mongometa__:
|
715 |
session = main_orm_session
|
761 |
session = main_orm_session
|
716 |
name='grant'
|
762 |
name='grant'
|
717 |
indexes = [ 'short' ]
|
763 |
indexes = [ 'short' ]
|
718 |
type_s = 'Generic Award Grant'
|
764 |
type_s = 'Generic Award Grant'
|