~jjacobs/+junk/flyingcircus

« back to all changes in this revision

Viewing changes to circus/qdb.py

  • Committer: Jonathan Jacobs
  • Date: 2009-07-18 23:12:51 UTC
  • Revision ID: korpse@slipgate.za.net-20090718231251-20680t6zolezfxvo
Implement demoting moderators.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import itertools, random, re
2
 
from zope.interface import implements
3
 
 
4
 
from epsilon.extime import Time
5
 
 
6
 
from twisted.python.components import registerAdapter
7
 
 
8
 
from nevow import rend, static, tags, url
9
 
from nevow.athena import LiveElement, expose
10
 
from nevow.inevow import IResource
11
 
from nevow.page import renderer
12
 
from nevow.vhost import VHostMonsterResource
13
 
 
14
 
from axiom import batch, userbase
15
 
from axiom.attributes import AND, integer, reference, text, timestamp
16
 
from axiom.dependency import dependsOn
17
 
from axiom.item import Item, transacted
18
 
from axiom.scheduler import Scheduler
19
 
from axiom.upgrade import registerAttributeCopyingUpgrader
20
 
 
21
 
from xmantissa.fulltext import SQLiteIndexer
22
 
from xmantissa.ixmantissa import (IFulltextIndexable, INavigableElement,
23
 
    INavigableFragment, ISessionlessSiteRootPlugin, ITemplateNameResolver)
24
 
from xmantissa.scrolltable import InequalityModel
25
 
from xmantissa.sharing import getEveryoneRole, itemFromProxy
26
 
from xmantissa.webapp import PrivateApplication
27
 
from xmantissa.webnav import Tab
28
 
from xmantissa.websharing import linkTo
29
 
from xmantissa.website import PrefixURLMixin
30
 
from xmantissa.webtheme import ThemedDocumentFactory
31
 
 
32
 
from methanal.model import Model, ValueParameter
33
 
from methanal.view import CheckboxInput, LiveForm, SelectInput, TextInput
34
 
 
35
 
from circus.color import colorizeLine
36
 
from circus.icircus import (IQuote, IQuoteAdder, IQuoteDatabase,
37
 
    IQuoteModeration, IQuoteSearcher)
38
 
from circus.roles import getModeratorsRole, getUserRole, inModerators
39
 
from circus.util import getServiceProvider
40
 
from circus.widgets import TabView
41
 
 
42
 
 
43
 
 
44
 
class Redirector(Item):
45
 
    """
46
 
    Redirect users away from C{/private}.
47
 
    """
48
 
    implements(INavigableElement)
49
 
 
50
 
    typeName = 'flyingcircus_redirector'
51
 
    schemaVersion = 1
52
 
    powerupInterfaces = [INavigableElement]
53
 
 
54
 
    privateApplication = dependsOn(PrivateApplication)
55
 
 
56
 
 
57
 
    def getTabs(self):
58
 
        return [Tab('Quotes', self.storeID, 1.0, authoritative=True, children=[
59
 
                    Tab('Overview', self.storeID, 1.0)])]
60
 
 
61
 
 
62
 
 
63
 
class RedirectorResource(object):
64
 
    implements(IResource)
65
 
 
66
 
 
67
 
    def __init__(self, redirector):
68
 
        pass
69
 
 
70
 
 
71
 
    def renderHTTP(self, ctx):
72
 
        # XXX: We should probably be generating a link from the appstore share
73
 
        # item itself.
74
 
        return url.root.child('FlyingCircus')
75
 
 
76
 
registerAdapter(RedirectorResource, Redirector, IResource)
77
 
 
78
 
 
79
 
 
80
 
class MyQuotes(Item):
81
 
    """
82
 
    Personalised quote view.
83
 
    """
84
 
    implements(INavigableElement)
85
 
 
86
 
    typeName = 'flyingcircus_myquotes'
87
 
    schemaVersion = 1
88
 
    powerupInterfaces = [INavigableElement]
89
 
 
90
 
    privateApplication = dependsOn(PrivateApplication)
91
 
 
92
 
 
93
 
    def getTabs(self):
94
 
        return [Tab('Quotes', self.storeID, 1.0, authoritative=False, children=[
95
 
                    Tab('My Quotes', self.storeID, 0.8)])]
96
 
 
97
 
 
98
 
 
99
 
class MyQuotesView(TabView):
100
 
    """
101
 
    Personalised quote view.
102
 
    """
103
 
    implements(INavigableFragment)
104
 
 
105
 
    title = u'Slipgate Quote Database - My Quotes'
106
 
    viewTitle = u'My Quotes'
107
 
 
108
 
    LIMIT = 5
109
 
 
110
 
 
111
 
    def __init__(self, myquotes):
112
 
        self.myquotes = myquotes
113
 
        self.store = self.myquotes.store
114
 
 
115
 
        self.resolver = ITemplateNameResolver(self.store.parent)
116
 
 
117
 
        quoteDB = getServiceProvider(self.store, IQuoteDatabase)
118
 
        userID = u'@'.join(userbase.getAccountNames(self.store).next())
119
 
 
120
 
        appStore = quoteDB.store
121
 
 
122
 
        role = getUserRole(appStore, userID)
123
 
 
124
 
        def pagerTab(title, status, allUsers=False, sortAscending=False):
125
 
            criteria = [Quote.status == status]
126
 
            if not allUsers:
127
 
                criteria.append(Quote.userID == userID)
128
 
 
129
 
            baseConstraint = AND(*criteria)
130
 
            pager = QuotePager(store=appStore,
131
 
                               resolver=self.resolver,
132
 
                               role=role,
133
 
                               attribute=Quote.added,
134
 
                               sortAscending=sortAscending,
135
 
                               limit=self.LIMIT,
136
 
                               baseConstraint=baseConstraint)
137
 
            return QuotesTab(title=title,
138
 
                             content=pager,
139
 
                             resolver=self.resolver)
140
 
 
141
 
        tabs = [
142
 
            pagerTab(u'Accepted', u'accepted'),
143
 
            pagerTab(u'Rejected', u'rejected'),
144
 
            ]
145
 
 
146
 
        if inModerators(role):
147
 
            tabs.append(pagerTab(u'Moderation', u'moderate', True, True))
148
 
 
149
 
        super(MyQuotesView, self).__init__(
150
 
            resolver=self.resolver,
151
 
            tabs=tabs)
152
 
 
153
 
registerAdapter(MyQuotesView, MyQuotes, INavigableFragment)
154
 
 
155
 
 
156
 
 
157
 
class QuoteDB(Item):
158
 
    implements(IQuoteAdder, IQuoteDatabase, IQuoteSearcher)
159
 
 
160
 
    typeName = 'flyingcircus_quotedb'
161
 
    schemaVersion = 3
162
 
    powerupInterfaces = [IQuoteDatabase]
163
 
 
164
 
    lastQid = integer(default=0)
165
 
 
166
 
    scheduler = dependsOn(Scheduler)
167
 
    searchIndexer = dependsOn(SQLiteIndexer)
168
 
 
169
 
 
170
 
    def getQuotesByIDs(self, quoteIDs, sort=None):
171
 
        """
172
 
        Query for L{Quote}s by their IDs.
173
 
        """
174
 
        return self.store.query(Quote,
175
 
                                Quote.qid.oneOf(quoteIDs),
176
 
                                sort=sort)
177
 
 
178
 
 
179
 
    # IQuoteAdder
180
 
    @transacted
181
 
    def addQuote(self, content, userID):
182
 
        self.lastQid = self.lastQid + 1
183
 
        quote = Quote(store=self.store,
184
 
                      qid=self.lastQid,
185
 
                      content=content,
186
 
                      userID=userID)
187
 
 
188
 
        createParticipants(quote)
189
 
        return shareQuote(quote)
190
 
 
191
 
 
192
 
    # IQuoteDatabase
193
 
    def getQuote(self, qid):
194
 
        return self.store.findUnique(Quote,
195
 
                                     Quote.qid == qid)
196
 
 
197
 
 
198
 
    def getRandomQuotes(self, limit=None):
199
 
        query = self.store.query(Quote,
200
 
                                 Quote.status != u'rejected')
201
 
 
202
 
        numQuotes = query.count()
203
 
        if limit is not None:
204
 
            numQuotes = min(numQuotes, limit)
205
 
 
206
 
        # XXX: This isn't very efficient.
207
 
        quoteIDs = list(query.getColumn('qid'))
208
 
        quoteIDs = random.sample(quoteIDs, numQuotes)
209
 
        return self.getQuotesByIDs(quoteIDs)
210
 
 
211
 
 
212
 
    # IQuoteSearcher
213
 
    def search(self, term, sort=None, limit=None):
214
 
        def getQuotes(results):
215
 
            quoteIDs = [r.uniqueIdentifier for r in results]
216
 
            return self.getQuotesByIDs(quoteIDs, sort)
217
 
 
218
 
        return self.searchIndexer.search(term, count=limit
219
 
            ).addCallback(getQuotes)
220
 
 
221
 
registerAttributeCopyingUpgrader(QuoteDB, 1, 2)
222
 
registerAttributeCopyingUpgrader(QuoteDB, 2, 3)
223
 
 
224
 
 
225
 
 
226
 
class QuoteDBView(LiveElement):
227
 
    """
228
 
    View for listing quotes from an L{IQuoteDatabase}.
229
 
    """
230
 
    implements(INavigableFragment)
231
 
 
232
 
    title = u'Slipgate Quote Database'
233
 
    docFactory = ThemedDocumentFactory('qdb-view', 'resolver')
234
 
 
235
 
    LIMIT = 15
236
 
 
237
 
 
238
 
    def __init__(self, quoteDB, userID=None):
239
 
        super(QuoteDBView, self).__init__()
240
 
        self.quoteDB = quoteDB
241
 
        self.userID = userID
242
 
 
243
 
        self.appStore = itemFromProxy(self.quoteDB).store
244
 
        self.resolver = ITemplateNameResolver(self.appStore.parent)
245
 
        self.role = getUserRole(self.appStore, userID)
246
 
 
247
 
 
248
 
    @renderer
249
 
    def content(self, req, tag):
250
 
        def firstOr(seq, default=None):
251
 
            if not seq:
252
 
                return default
253
 
            return seq[0]
254
 
 
255
 
        args = req.args
256
 
        sortType = firstOr(args.get('sort'))
257
 
        sortAscending = firstOr(args.get('dir'), 'desc') != 'desc'
258
 
        limit = firstOr(args.get('limit'), self.LIMIT)
259
 
        limit = min(int(limit), self.LIMIT)
260
 
 
261
 
        baseConstraint = Quote.status != u'rejected'
262
 
 
263
 
        elem = None
264
 
        if sortType is not None:
265
 
            attribute = translateSortType(sortType, None)
266
 
            if attribute is not None:
267
 
                elem = QuotePager(store=self.appStore,
268
 
                                  resolver=self.resolver,
269
 
                                  role=self.role,
270
 
                                  attribute=attribute,
271
 
                                  sortAscending=sortAscending,
272
 
                                  limit=limit,
273
 
                                  baseConstraint=baseConstraint)
274
 
            elif sortType == 'random':
275
 
                query = self.quoteDB.getRandomQuotes(limit)
276
 
                quotes = list(self.role.asAccessibleTo(query))
277
 
                elem = QuoteList(quotes=quotes,
278
 
                                 resolver=self.resolver)
279
 
        else:
280
 
            elem = QuoteDatabaseOverview(store=self.appStore,
281
 
                                         role=self.role,
282
 
                                         resolver=self.resolver,
283
 
                                         userID=self.userID)
284
 
 
285
 
        elem.setFragmentParent(self)
286
 
        return tag[elem]
287
 
 
288
 
 
289
 
    # INavigableFragment
290
 
    def customizeFor(self, userID):
291
 
        return type(self)(self.quoteDB, userID)
292
 
 
293
 
registerAdapter(QuoteDBView, IQuoteDatabase, INavigableFragment)
294
 
 
295
 
 
296
 
 
297
 
class QuoteSearchView(LiveElement):
298
 
    """
299
 
    A view for performing quote searches.
300
 
    """
301
 
    implements(INavigableFragment)
302
 
 
303
 
    title = u'Slipgate Quote Database - Search'
304
 
    docFactory = ThemedDocumentFactory('qdb-search', 'resolver')
305
 
    jsClass = u'FlyingCircus.Search.QuoteSearch'
306
 
 
307
 
    LIMIT = 30
308
 
 
309
 
 
310
 
    def __init__(self, quoteDB, userID=None):
311
 
        super(QuoteSearchView, self).__init__()
312
 
        self.quoteDB = quoteDB
313
 
        self.userID = userID
314
 
 
315
 
        appStore = itemFromProxy(quoteDB).store
316
 
        self.resolver = ITemplateNameResolver(appStore.parent)
317
 
        self.role = getUserRole(appStore, self.userID)
318
 
 
319
 
 
320
 
    def performSearch(self, term, sortType, sortAscending):
321
 
        def makeResultList(query):
322
 
            quotes = self.role.asAccessibleTo(query)
323
 
            elem = QuoteList(quotes=quotes,
324
 
                             resolver=self.resolver)
325
 
            elem.setFragmentParent(self)
326
 
            return elem, query.count()
327
 
 
328
 
        sort = translateSortType(sortType, sortAscending)
329
 
 
330
 
        return self.quoteDB.search(term=term, sort=sort, limit=self.LIMIT
331
 
            ).addCallback(makeResultList)
332
 
 
333
 
 
334
 
    @renderer
335
 
    def searchForm(self, req, tag):
336
 
        model = Model(callback=self.performSearch,
337
 
                      params=[ValueParameter(name='term',
338
 
                                             doc=u'Terms',
339
 
                                             value=None),
340
 
                              ValueParameter(name='sortType',
341
 
                                             doc=u'Sort by',
342
 
                                             value=u'rating'),
343
 
                              ValueParameter(name='sortAscending',
344
 
                                             doc=u'Ascending?',
345
 
                                             value=False)],
346
 
                      doc=u'Search')
347
 
        form = LiveForm(store=None, model=model, fragmentParent=self)
348
 
        form.jsClass = u'FlyingCircus.Search.QuoteSearchForm'
349
 
        TextInput(parent=form, name='term')
350
 
 
351
 
        sortValues = [
352
 
            (u'rating', u'Rating'),
353
 
            (u'date',   u'Date')]
354
 
        SelectInput(parent=form, name='sortType', values=sortValues)
355
 
        CheckboxInput(parent=form, name='sortAscending')
356
 
 
357
 
        return tag[form]
358
 
 
359
 
 
360
 
    # INavigableFragment
361
 
    def customizeFor(self, userID):
362
 
        return type(self)(self.quoteDB, userID)
363
 
 
364
 
registerAdapter(QuoteSearchView, IQuoteSearcher, INavigableFragment)
365
 
 
366
 
 
367
 
 
368
 
class QuoteAdderView(LiveElement):
369
 
    """
370
 
    View for L{IQuoteAdder}s.
371
 
 
372
 
    Providers users with an interface for submitting a new quote.
373
 
    """
374
 
    implements(INavigableFragment)
375
 
 
376
 
    title = u'Slipgate Quote Database - Add Quote'
377
 
    docFactory = ThemedDocumentFactory('add-quote', 'resolver')
378
 
    jsClass = u'FlyingCircus.Quotes.QuoteAdder'
379
 
 
380
 
 
381
 
    def __init__(self, quoteAdder):
382
 
        super(QuoteAdderView, self).__init__()
383
 
        self.quoteAdder = quoteAdder
384
 
 
385
 
        appStore = itemFromProxy(self.quoteAdder).store
386
 
        self.resolver = ITemplateNameResolver(appStore.parent)
387
 
        self.userID = None
388
 
 
389
 
 
390
 
    @expose
391
 
    def addQuote(self, content):
392
 
        quote = self.quoteAdder.addQuote(content, self.userID)
393
 
        url = linkTo(quote)
394
 
        return unicode(url)
395
 
 
396
 
 
397
 
    # INavigableFragment
398
 
    def customizeFor(self, userID):
399
 
        self.userID = userID
400
 
        return self
401
 
 
402
 
registerAdapter(QuoteAdderView, IQuoteAdder, INavigableFragment)
403
 
 
404
 
 
405
 
 
406
 
class Quote(Item):
407
 
    implements(IQuote, IQuoteModeration, IFulltextIndexable)
408
 
 
409
 
    typeName = 'flyingcircus_quote'
410
 
    schemaVersion = 2
411
 
 
412
 
    qid = integer(allowNone=False, indexed=True)
413
 
    content = text(allowNone=False)
414
 
    added = timestamp(allowNone=False, indexed=True, defaultFactory=lambda: Time())
415
 
    votesFor = integer(allowNone=False, default=0)
416
 
    votesAgainst = integer(allowNone=False, default=0)
417
 
    rating = integer(allowNone=False, default=0, indexed=True)
418
 
    votes = integer(allowNone=False, default=0)
419
 
    userID = text()
420
 
    status = text(allowNone=False, default=u'accepted')
421
 
 
422
 
 
423
 
    def __repr__(self):
424
 
        return '<%s qid=%s rating=%d votes=%d added=%r>' % (
425
 
            type(self).__name__,
426
 
            self.qid,
427
 
            self.rating,
428
 
            self.votes,
429
 
            self.added.asHumanly())
430
 
 
431
 
 
432
 
    def stored(self):
433
 
        # Tell the batch processor that we have data to index.
434
 
        qs = self.store.findUnique(QuoteSource)
435
 
        qs.itemAdded()
436
 
 
437
 
 
438
 
    # IQuote
439
 
    @transacted
440
 
    def plus(self):
441
 
        self.votesFor = self.votesFor + 1
442
 
        self.rating = self.rating + 1
443
 
        self.votes = self.votes + 1
444
 
 
445
 
 
446
 
    @transacted
447
 
    def minus(self):
448
 
        self.votesAgainst = self.votesAgainst + 1
449
 
        self.rating = self.rating - 1
450
 
        self.votes = self.votes + 1
451
 
 
452
 
 
453
 
    @transacted
454
 
    def requestModeration(self):
455
 
        self.status = u'moderate'
456
 
 
457
 
 
458
 
    def getParticipants(self):
459
 
        return self.store.query(Clown, Clown.quote == self).getColumn('nickname')
460
 
 
461
 
 
462
 
    def getUsername(self):
463
 
        userID = self.userID
464
 
        if userID is not None:
465
 
            return userID.split(u'@', 1)[0]
466
 
        return None
467
 
 
468
 
 
469
 
    # IQuoteModeration
470
 
    @transacted
471
 
    def reject(self):
472
 
        self.status = u'rejected'
473
 
 
474
 
 
475
 
    @transacted
476
 
    def accept(self):
477
 
        self.status = u'accepted'
478
 
 
479
 
 
480
 
    # IFulltextIndexable
481
 
    def uniqueIdentifier(self):
482
 
        return str(self.qid)
483
 
 
484
 
 
485
 
    def textParts(self):
486
 
        return [self.content]
487
 
 
488
 
registerAttributeCopyingUpgrader(Quote, 1, 2)
489
 
 
490
 
QuoteSource = batch.processor(Quote)
491
 
 
492
 
 
493
 
 
494
 
class QuoteView(LiveElement):
495
 
    """
496
 
    Navigable resource for a single L{IQuote}.
497
 
 
498
 
    Visiting the C{raw} child resource results in a stripped down quote.
499
 
    """
500
 
    implements(INavigableFragment)
501
 
 
502
 
    docFactory = ThemedDocumentFactory('qdb-view', 'resolver')
503
 
 
504
 
 
505
 
    def __init__(self, quote):
506
 
        super(QuoteView, self).__init__()
507
 
        self.quote = quote
508
 
 
509
 
        appStore = itemFromProxy(self.quote).store
510
 
        self.resolver = ITemplateNameResolver(appStore.parent)
511
 
 
512
 
 
513
 
    def locateChild(self, ctx, segments):
514
 
        if segments and segments[0] == 'raw':
515
 
            quote = self.quote
516
 
            lines = quote.content.splitlines()
517
 
            lines.insert(0, '#%s (%s/%s)' % (quote.qid, quote.rating, quote.votes))
518
 
            text = '\n'.join(lines)
519
 
            return static.Data(text.encode('utf-8'), 'text/plain; charset=UTF-8'), ()
520
 
 
521
 
        return rend.NotFound
522
 
 
523
 
 
524
 
    @property
525
 
    def title(self):
526
 
        return u'Slipgate Quote Database - #%s' % (self.quote.qid,)
527
 
 
528
 
 
529
 
    @renderer
530
 
    def content(self, req, tag):
531
 
        elem = QuoteElement(quote=self.quote,
532
 
                            resolver=self.resolver)
533
 
        elem.setFragmentParent(self)
534
 
        return tag[elem]
535
 
 
536
 
registerAdapter(QuoteView, IQuote, INavigableFragment)
537
 
 
538
 
 
539
 
 
540
 
class Clown(Item):
541
 
    """
542
 
    A participant in a L{Quote}.
543
 
    """
544
 
    typeName = 'flyingcircus_clown'
545
 
    schemaVersion = 1
546
 
 
547
 
    quote = reference(doc="""
548
 
    L{Quote} this participant is involved in.
549
 
    """, allowNone=False, reftype=Quote, indexed=True)
550
 
 
551
 
    nickname = text(doc="""
552
 
    The participant's nickname.
553
 
    """, allowNone=False)
554
 
 
555
 
 
556
 
 
557
 
class QuotesTab(LiveElement):
558
 
    """
559
 
    A simple container, intended for use with C{FragmentCollector}.
560
 
    """
561
 
    implements(INavigableFragment)
562
 
 
563
 
    docFactory = ThemedDocumentFactory('quotes-tab', 'resolver')
564
 
 
565
 
 
566
 
    def __init__(self, title, content, resolver):
567
 
        """
568
 
        Initialise the container.
569
 
 
570
 
        @type title: C{unicode}
571
 
        @param title: Title for the tab
572
 
 
573
 
        @type content: C{renderable}
574
 
        @param content: Renderable tab content
575
 
 
576
 
        @type resolver: C{ITemplateNameResolver}
577
 
        @param resolver: Template resolver
578
 
        """
579
 
        super(QuotesTab, self).__init__()
580
 
        self.title = title.encode('utf-8')
581
 
        self.content = content
582
 
        self.content.setFragmentParent(self)
583
 
        self.resolver = resolver
584
 
 
585
 
 
586
 
    @renderer
587
 
    def tabContent(self, req, tag):
588
 
        return tag[self.content]
589
 
 
590
 
 
591
 
    # INavigableFragment
592
 
    def head(self):
593
 
        return None
594
 
 
595
 
 
596
 
 
597
 
class QuoteDatabaseOverview(LiveElement):
598
 
    """
599
 
    Overview for a quote database.
600
 
    """
601
 
    docFactory = ThemedDocumentFactory('qdb-overview', 'resolver')
602
 
 
603
 
    LIMIT = 2
604
 
 
605
 
 
606
 
    def __init__(self, store, role, resolver, userID=None):
607
 
        super(QuoteDatabaseOverview, self).__init__()
608
 
        self.store = store
609
 
        self.role = role
610
 
        self.resolver = resolver
611
 
        self.userID = userID
612
 
 
613
 
 
614
 
    def makePager(self, attribute):
615
 
        baseConstraint = Quote.status != u'rejected'
616
 
        elem = QuotePager(store=self.store,
617
 
                          resolver=self.resolver,
618
 
                          role=self.role,
619
 
                          attribute=attribute,
620
 
                          sortAscending=False,
621
 
                          limit=self.LIMIT,
622
 
                          baseConstraint=baseConstraint)
623
 
        elem.setFragmentParent(self)
624
 
        return elem
625
 
 
626
 
 
627
 
    @renderer
628
 
    def recent(self, req, tag):
629
 
        return tag[self.makePager(Quote.added)]
630
 
 
631
 
 
632
 
    @renderer
633
 
    def top(self, req, tag):
634
 
        return tag[self.makePager(Quote.rating)]
635
 
 
636
 
 
637
 
    @renderer
638
 
    def signupNag(self, req, tag):
639
 
        if self.userID is None:
640
 
            return tag
641
 
        return []
642
 
 
643
 
 
644
 
 
645
 
class QuoteElement(LiveElement):
646
 
    """
647
 
    View for a single quote.
648
 
 
649
 
    An Athena interface for voting and retrieving the quote rating is exposed.
650
 
 
651
 
    If the quote has participant information, it will be used for highlighting.
652
 
    """
653
 
    docFactory = ThemedDocumentFactory('quote', 'resolver')
654
 
    jsClass = u'FlyingCircus.Quotes.Quote'
655
 
 
656
 
    _statusIndicators = {
657
 
        u'rejected': u'!',
658
 
        u'moderate': u'*'}
659
 
 
660
 
 
661
 
    def __init__(self, quote, resolver):
662
 
        """
663
 
        Initialise the element.
664
 
 
665
 
        @type quote: C{SharedProxy} for an L{IQuote} interface
666
 
 
667
 
        @type resolver: C{ITemplateNameResolver}
668
 
        """
669
 
        super(QuoteElement, self).__init__()
670
 
        self.quote = quote
671
 
        self.resolver = resolver
672
 
 
673
 
 
674
 
    def getInitialArguments(self):
675
 
        return [self.getRating()]
676
 
 
677
 
 
678
 
    def getStatusInfo(self):
679
 
        """
680
 
        Get information related to the current quote status.
681
 
 
682
 
        @rtype: C{(unicode, unicode)}
683
 
        @return: C{(statusIndicator, statusCSSClassName)}
684
 
        """
685
 
        status = self.quote.status
686
 
        return self._statusIndicators.get(status, u''), u'status-%s' % (status,)
687
 
 
688
 
 
689
 
    def colorizeContent(self):
690
 
        """
691
 
        Highlight nicknames in the quote content.
692
 
 
693
 
        @rtype: C{iterable} of renderable elements, each one representing
694
 
            a line from the quote content
695
 
        """
696
 
        def nickNode(nickname, color):
697
 
            style = 'color: #%x%x%x' % color
698
 
            return tags.span(style=style)[nickname]
699
 
 
700
 
        return (colorizeLine(nickNode, line, self.quote.getParticipants())
701
 
                for line in self.quote.content.strip().splitlines())
702
 
 
703
 
 
704
 
    @expose
705
 
    def getRating(self):
706
 
        return self.quote.rating, self.quote.votes
707
 
 
708
 
 
709
 
    @expose
710
 
    def plus(self):
711
 
        self.quote.plus()
712
 
        return self.getRating()
713
 
 
714
 
 
715
 
    @expose
716
 
    def minus(self):
717
 
        self.quote.minus()
718
 
        return self.getRating()
719
 
 
720
 
 
721
 
    @expose
722
 
    def moderate(self):
723
 
        self.quote.requestModeration()
724
 
        return self.getStatusInfo()
725
 
 
726
 
 
727
 
    @expose
728
 
    def reject(self):
729
 
        self.quote.reject()
730
 
        return self.getStatusInfo()
731
 
 
732
 
 
733
 
    @expose
734
 
    def accept(self):
735
 
        self.quote.accept()
736
 
        return self.getStatusInfo()
737
 
 
738
 
 
739
 
    @renderer
740
 
    def quoteInfo(self, req, tag):
741
 
        quote = self.quote
742
 
        tag.fillSlots('permalink', linkTo(quote))
743
 
        tag.fillSlots('qid', quote.qid)
744
 
        statusIndicator, statusClass = self.getStatusInfo()
745
 
        tag.fillSlots('statusClass', statusClass)
746
 
        tag.fillSlots('statusIndicator', statusIndicator)
747
 
        tag.fillSlots('created', quote.added.asHumanly())
748
 
        username = quote.getUsername()
749
 
        if username is not None:
750
 
            username = [u'by ', tags.strong[username]]
751
 
        else:
752
 
            username = []
753
 
        tag.fillSlots('user', username)
754
 
 
755
 
        return tag
756
 
 
757
 
 
758
 
    @renderer
759
 
    def quoteContent(self, req, tag):
760
 
        content = itertools.izip(self.colorizeContent(),
761
 
                                 itertools.repeat(tags.br))
762
 
        return tag[content]
763
 
 
764
 
 
765
 
    @renderer
766
 
    def moderation(self, req, tag):
767
 
        status = self.quote.status
768
 
 
769
 
        def buttons():
770
 
            if IQuoteModeration.providedBy(self.quote):
771
 
                if status != u'rejected':
772
 
                    yield tag.onePattern('rejectButton')
773
 
                if status == u'moderate':
774
 
                    yield tag.onePattern('acceptButton')
775
 
            elif status == u'accepted':
776
 
                yield tag.onePattern('moderateButton')
777
 
 
778
 
        return tag[buttons()]
779
 
 
780
 
 
781
 
 
782
 
class PagerInequalityModel(InequalityModel):
783
 
    """
784
 
    InequalityModel for paginated quote views.
785
 
    """
786
 
    def __init__(self, store, role, attribute, sortAscending, baseConstraint):
787
 
        super(PagerInequalityModel, self).__init__(store=store,
788
 
                                                   itemType=Quote,
789
 
                                                   baseConstraint=baseConstraint,
790
 
                                                   columns=[attribute],
791
 
                                                   defaultSortColumn=attribute,
792
 
                                                   defaultSortAscending=False)
793
 
        self.role = role
794
 
        self.sortAscending = sortAscending
795
 
 
796
 
 
797
 
    def constructRows(self, query):
798
 
        rows = list(self.role.asAccessibleTo(query))
799
 
        if not self.sortAscending:
800
 
            rows = list(reversed(rows))
801
 
        return rows
802
 
 
803
 
 
804
 
 
805
 
class QuotePager(LiveElement):
806
 
    """
807
 
    A paginated quote list view.
808
 
    """
809
 
    docFactory = ThemedDocumentFactory('quote-pager', 'resolver')
810
 
    jsClass = u'FlyingCircus.Quotes.QuotePager'
811
 
 
812
 
 
813
 
    def __init__(self, store, resolver, role, attribute, sortAscending, limit, baseConstraint=None):
814
 
        """
815
 
        Initialise the pager element.
816
 
 
817
 
        @type store: Axiom store
818
 
        @param store: Store to query for L{Quote}s in
819
 
 
820
 
        @type resolver: C{ITemplateNameResolver}
821
 
 
822
 
        @type role: C{xmantissa.sharing.Role}
823
 
        @param role: Role to use when exposing shared items to the view
824
 
 
825
 
        @type attribute: Axiom item attribute
826
 
        @param attribute: L{Quote} attribute to use for queries and sorting
827
 
 
828
 
        @type sortAscending: C{boolean}
829
 
        @param sortAscending: Determine whether to sort the query results
830
 
            ascending or not
831
 
 
832
 
        @type limit: C{int}
833
 
        @param limit: Number of items per page
834
 
 
835
 
        @type baseConstraint: Axiom query or C{None}
836
 
        @param baseConstraint: Base constraint to apply to queries or C{None}
837
 
        """
838
 
        super(QuotePager, self).__init__()
839
 
        self.store = store
840
 
        self.resolver = resolver
841
 
        self.attribute = attribute
842
 
        self.sortAscending = sortAscending
843
 
        self.limit = limit
844
 
 
845
 
        self.firstValue = self.lastValue = None
846
 
        self.imodel = PagerInequalityModel(store=self.store,
847
 
                                           role=role,
848
 
                                           attribute=self.attribute,
849
 
                                           sortAscending=sortAscending,
850
 
                                           baseConstraint=baseConstraint)
851
 
 
852
 
        getters = [self.imodel.rowsBeforeItem, self.imodel.rowsAfterItem]
853
 
        getNext, getPrev = getters[sortAscending], getters[not sortAscending]
854
 
 
855
 
        self.getNext = lambda: getNext(self.lastValue, self.limit)
856
 
        self.getPrev = lambda: getPrev(self.firstValue, self.limit)
857
 
 
858
 
 
859
 
    def makeQuoteList(self, quotes, allowEmpty=False):
860
 
        """
861
 
        Create a L{QuoteList} widget.
862
 
 
863
 
        @type quotes: C{sequence} of C{SharedProxy}s implementing L{IQuote}
864
 
 
865
 
        @param allowEmpty: Create an empty L{QuoteList} if L{quotes} is empty
866
 
        """
867
 
        if not quotes and not allowEmpty:
868
 
            return None
869
 
 
870
 
        if quotes:
871
 
            self.firstValue = itemFromProxy(quotes[0])
872
 
            self.lastValue = itemFromProxy(quotes[-1])
873
 
 
874
 
        elem = QuoteList(quotes=quotes,
875
 
                         resolver=self.resolver)
876
 
        elem.setFragmentParent(self)
877
 
        return elem
878
 
 
879
 
 
880
 
    @renderer
881
 
    def quoteList(self, req, tag):
882
 
        if self.sortAscending:
883
 
            quotes = self.imodel.rowsAfterValue(None, self.limit)
884
 
        else:
885
 
            quotes = self.imodel.rowsBeforeValue(None, self.limit)
886
 
        return tag[self.makeQuoteList(quotes, True)]
887
 
 
888
 
 
889
 
    @expose
890
 
    def nextPage(self):
891
 
        """
892
 
        Retrieve a widget containing the next page of quotes.
893
 
        """
894
 
        quotes = []
895
 
        if self.lastValue is not None:
896
 
            quotes = self.getNext()
897
 
        return self.makeQuoteList(quotes)
898
 
 
899
 
 
900
 
    @expose
901
 
    def prevPage(self):
902
 
        """
903
 
        Retrieve a widget containing the previous page of quotes.
904
 
        """
905
 
        quotes = []
906
 
        if self.firstValue is not None:
907
 
            quotes = self.getPrev()
908
 
        return self.makeQuoteList(quotes)
909
 
 
910
 
 
911
 
 
912
 
class QuoteList(LiveElement):
913
 
    """
914
 
    View for multiple quotes.
915
 
 
916
 
    Quotes will be zebra-striped and if there are no quotes an "empty" view
917
 
    will be rendered.
918
 
    """
919
 
    docFactory = ThemedDocumentFactory('quote-list', 'resolver')
920
 
 
921
 
 
922
 
    def __init__(self, quotes, resolver):
923
 
        """
924
 
        Initialise quote list element.
925
 
 
926
 
        @type quotes: C{iterable} of C{SharedProxy}s for L{IQuote}
927
 
        """
928
 
        super(QuoteList, self).__init__()
929
 
        self.quotes = list(quotes)
930
 
        self.resolver = resolver
931
 
 
932
 
 
933
 
    @renderer
934
 
    def quoteList(self, req, tag):
935
 
        if not self.quotes:
936
 
            return tag[tag.onePattern('empty')]
937
 
 
938
 
        oddeven = itertools.cycle(['even', 'odd'])
939
 
 
940
 
        def content():
941
 
            for quote, cls in itertools.izip(self.quotes, oddeven):
942
 
                elem = QuoteElement(quote=quote,
943
 
                                    resolver=self.resolver)
944
 
                elem.setFragmentParent(self)
945
 
                yield tags.div(class_=cls)[elem]
946
 
 
947
 
        return tag[content()]
948
 
 
949
 
 
950
 
 
951
 
class VHost(Item, PrefixURLMixin):
952
 
    implements(ISessionlessSiteRootPlugin)
953
 
 
954
 
    typeName = 'flyingcircus_vhost'
955
 
    schemaVersion = 1
956
 
 
957
 
    sessionless = True
958
 
 
959
 
    prefixURL = text(default=u'vhost')
960
 
 
961
 
    def createResource(self):
962
 
        return VHostMonsterResource()
963
 
 
964
 
 
965
 
 
966
 
_nicknamePatterns = [
967
 
    re.compile(r'<[ +%@&~]?(.*?) ?> '),  # Addressing: < foo> hi  <@bar> hi there
968
 
    re.compile(r'^\s*\* (.*?) '),        # Actions: * foo does a thing
969
 
    ]
970
 
 
971
 
def extractParticipants(content):
972
 
    """
973
 
    Extract a list of unique nicknames that participate in a quote.
974
 
 
975
 
    @param content: Text representation of a quote's content
976
 
    @type content: C{unicode}
977
 
 
978
 
    @rtype: C{tuple} of C{unicode}
979
 
    @return: The featured nicknames
980
 
    """
981
 
    nicknames = set()
982
 
 
983
 
    for line in content.splitlines():
984
 
        for pattern in _nicknamePatterns:
985
 
            match = pattern.search(line)
986
 
            if match is not None and match.group(1):
987
 
                nicknames.add(match.group(1))
988
 
                break
989
 
 
990
 
    return tuple(nicknames)
991
 
 
992
 
 
993
 
 
994
 
def shareQuote(quote):
995
 
    """
996
 
    Share a L{Quote} item.
997
 
    """
998
 
    shareID = unicode(quote.qid)
999
 
 
1000
 
    sharedQuote = getEveryoneRole(quote.store).shareItem(
1001
 
        sharedItem=quote,
1002
 
        shareID=shareID,
1003
 
        interfaces=[IQuote])
1004
 
 
1005
 
    modRole = getModeratorsRole(quote.store)
1006
 
    modRole.shareItem(
1007
 
        sharedItem=quote,
1008
 
        shareID=shareID,
1009
 
        interfaces=[IQuoteModeration])
1010
 
 
1011
 
    return sharedQuote
1012
 
 
1013
 
 
1014
 
 
1015
 
@transacted
1016
 
def createParticipants(quote):
1017
 
    """
1018
 
    Create L{Clown} items for each participant in C{quote}.
1019
 
 
1020
 
    @type quote: L{Quote}
1021
 
    """
1022
 
    store = quote.store
1023
 
    store.query(Clown, Clown.quote == quote).deleteFromStore();
1024
 
 
1025
 
    for nickname in extractParticipants(quote.content):
1026
 
        Clown(store=store,
1027
 
              quote=quote,
1028
 
              nickname=nickname)
1029
 
 
1030
 
 
1031
 
 
1032
 
_sortTypes = {
1033
 
    'date':   Quote.added,
1034
 
    'rating': Quote.rating}
1035
 
 
1036
 
def translateSortType(sortType, sortAscending):
1037
 
    attribute = _sortTypes.get(sortType)
1038
 
    if attribute is not None and sortAscending is not None:
1039
 
        direction = ['descending', 'ascending'][sortAscending]
1040
 
        attribute = getattr(attribute, direction)
1041
 
    return attribute