allura
リビジョン | a07e7448376895f998594023b8bafa6a76ec1524 (tree) |
---|---|
日時 | 2010-04-23 08:10:13 |
作者 | Jenny Steele <jsteele@geek...> |
コミッター | Jonathan T. Beard |
[#246] New theme for discussion.
Also noticed there was some missing functionality depicted by the mockups and added that in:
Sidebar links to reply, flag as spam, follow, and tag a thread.
Rss feeds for threads.
Tags for threads
Recently updated threads in sidebar
Help pages for forum permissions (placeholder) and markdown syntax
@@ -56,6 +56,16 @@ class RootController(object): | ||
56 | 56 | if results: count=results.hits |
57 | 57 | return dict(q=q, history=history, results=results or [], count=count) |
58 | 58 | |
59 | + @expose('forgediscussion.templates.markdown_syntax') | |
60 | + def markdown_syntax(self): | |
61 | + 'Static page explaining markdown.' | |
62 | + return dict() | |
63 | + | |
64 | + @expose('forgediscussion.templates.help') | |
65 | + def help(self): | |
66 | + 'Static help page.' | |
67 | + return dict() | |
68 | + | |
59 | 69 | @expose() |
60 | 70 | def _lookup(self, id, *remainder): |
61 | 71 | return ForumController(id), remainder |
@@ -1,6 +1,7 @@ | ||
1 | 1 | #-*- python -*- |
2 | 2 | import logging |
3 | 3 | import Image |
4 | +import pymongo | |
4 | 5 | |
5 | 6 | # Non-stdlib imports |
6 | 7 | import pkg_resources |
@@ -119,12 +120,26 @@ class ForgeDiscussionApp(Application): | ||
119 | 120 | |
120 | 121 | def sidebar_menu(self): |
121 | 122 | try: |
122 | - l = [SitemapEntry('Home', '.')] | |
123 | + l = [SitemapEntry('Home', c.app.url, ui_icon='home')] | |
124 | + # if we are in a thread, provide placeholder links to use in js | |
125 | + if '/thread/' in request.url: | |
126 | + l += [ | |
127 | + SitemapEntry('Reply to This', '#', ui_icon='comment', className='sidebar_thread_reply'), | |
128 | + SitemapEntry('Tag This', '#', ui_icon='tag', className='sidebar_thread_tag'), | |
129 | + SitemapEntry('Follow This', 'feed.rss', ui_icon='signal-diag'), | |
130 | + SitemapEntry('Mark as Spam', 'flag_as_spam', ui_icon='flag', className='sidebar_thread_spam') | |
131 | + ] | |
132 | + else: | |
133 | + l.append(SitemapEntry('Search', c.app.url+'search', ui_icon='search')) | |
123 | 134 | if has_artifact_access('admin', app=c.app)(): |
124 | - l.append(SitemapEntry('Admin', c.project.url()+'admin/'+self.config.options.mount_point)) | |
125 | - l.append(SitemapEntry('Search', 'search')) | |
126 | - l += [ SitemapEntry(f.name, f.url()) | |
127 | - for f in self.top_forums if (not f.deleted or has_artifact_access('configure', app=c.app)()) ] | |
135 | + l.append(SitemapEntry('Admin', c.project.url()+'admin/'+self.config.options.mount_point, ui_icon='wrench')) | |
136 | + l.append(SitemapEntry('Recent Topics')) | |
137 | + l += [ SitemapEntry(thread.subject, thread.url(), className='nav_child') | |
138 | + for thread in model.ForumThread.query.find().sort('mod_date', pymongo.DESCENDING).limit(3) | |
139 | + if (not thread.discussion.deleted or has_artifact_access('configure', app=c.app)()) ] | |
140 | + l.append(SitemapEntry('Forum Help')) | |
141 | + l.append(SitemapEntry('Forum Permissions', c.app.url + 'help', className='nav_child')) | |
142 | + l.append(SitemapEntry('Markdown Syntax', c.app.url + 'markdown_syntax', className='nav_child')) | |
128 | 143 | return l |
129 | 144 | except: # pragma no cover |
130 | 145 | log.exception('sidebar_menu') |
@@ -0,0 +1,19 @@ | ||
1 | +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | |
2 | + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
3 | +<html xmlns="http://www.w3.org/1999/xhtml" | |
4 | + xmlns:py="http://genshi.edgewall.org/" | |
5 | + xmlns:xi="http://www.w3.org/2001/XInclude"> | |
6 | + | |
7 | + <xi:include href="${g.pyforge_templates}/master.html"/> | |
8 | + <xi:include href="lib.html"/> | |
9 | + | |
10 | + <head> | |
11 | + <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/> | |
12 | + <title>Forum Permissions</title> | |
13 | + </head> | |
14 | + | |
15 | + <body> | |
16 | + <h1 class="title">Forum Permissions</h1> | |
17 | + <p>Help is coming soon.</p> | |
18 | + </body> | |
19 | +</html> |
@@ -0,0 +1,40 @@ | ||
1 | +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | |
2 | + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
3 | +<html xmlns="http://www.w3.org/1999/xhtml" | |
4 | + xmlns:py="http://genshi.edgewall.org/" | |
5 | + xmlns:xi="http://www.w3.org/2001/XInclude"> | |
6 | + | |
7 | + <xi:include href="${g.pyforge_templates}/master.html"/> | |
8 | + <xi:include href="lib.html"/> | |
9 | + | |
10 | + <head> | |
11 | + <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/> | |
12 | + <title>Markdown Syntax</title> | |
13 | + </head> | |
14 | + | |
15 | + <body> | |
16 | + <h1 class="title">Markdown Syntax</h1> | |
17 | + <p>You can use | |
18 | + <a href="http://daringfireball.net/projects/markdown/">MarkDown</a> | |
19 | + Syntax here</p> | |
20 | + <h4>Example Input</h4> | |
21 | + <pre style="background-color:#fff"> | |
22 | +# First-level heading | |
23 | + | |
24 | +Some *emphasized* and **strong** text | |
25 | + | |
26 | +#### Fourth-level heading | |
27 | + | |
28 | +</pre> | |
29 | + <h4>Example Rendering</h4> | |
30 | + <div style="background-color:#fff">${Markup(g.markdown.convert(''' | |
31 | +# First-level heading | |
32 | + | |
33 | +Some *emphasized* and **strong** text | |
34 | + | |
35 | +#### Fourth-level heading | |
36 | + | |
37 | +'''))}</div> | |
38 | + <a href="http://daringfireball.net/projects/markdown/syntax">More Examples</a> | |
39 | + </body> | |
40 | +</html> |
@@ -212,6 +212,16 @@ class TestForum(TestController): | ||
212 | 212 | r = self.app.get('/discussion/search') |
213 | 213 | r = self.app.get('/discussion/search', params=dict(q='foo')) |
214 | 214 | |
215 | + def test_render_help(self): | |
216 | + summary = 'test render help' | |
217 | + r = self.app.get('/discussion/help') | |
218 | + assert 'Forum Permissions' in r | |
219 | + | |
220 | + def test_render_markdown_syntax(self): | |
221 | + summary = 'test render markdown syntax' | |
222 | + r = self.app.get('/discussion/markdown_syntax') | |
223 | + assert 'Markdown Syntax' in r | |
224 | + | |
215 | 225 | def test_forum_subscribe(self): |
216 | 226 | r = self.app.get('/discussion/subscribe', params={ |
217 | 227 | 'forum-0.shortname':'TestForum', |
@@ -254,10 +264,24 @@ class TestForum(TestController): | ||
254 | 264 | def test_sidebar_menu(self): |
255 | 265 | r = self.app.get('/discussion/') |
256 | 266 | sidebarmenu = str(r.html.find('ul',{'id':'sidebarmenu'})) |
257 | - assert '<a href="." class=" ">Home</a>' in sidebarmenu | |
258 | - assert '<a href="/p/test/admin/discussion" class=" ">Admin</a>' in sidebarmenu | |
259 | - assert '<a href="search" class=" ">Search</a>' in sidebarmenu | |
260 | - assert '<a href="/p/test/discussion/TestForum/" class=" ">Test Forum</a>' in sidebarmenu | |
267 | + assert '<a href="/p/test/discussion/" class=" "><span class="ui-icon ui-icon-home"></span>Home</a>' in sidebarmenu | |
268 | + assert '<a href="/p/test/admin/discussion" class=" "><span class="ui-icon ui-icon-wrench"></span>Admin</a>' in sidebarmenu | |
269 | + assert '<a href="/p/test/discussion/search" class=" "><span class="ui-icon ui-icon-search"></span>Search</a>' in sidebarmenu | |
270 | + assert '<span class=" nav_head">Forum Help</span>' in sidebarmenu | |
271 | + assert '<a href="/p/test/discussion/help" class="nav_child ">Forum Permissions</a>' in sidebarmenu | |
272 | + assert '<a href="/p/test/discussion/markdown_syntax" class="nav_child ">Markdown Syntax</a>' in sidebarmenu | |
273 | + assert '<a href="#" class="sidebar_thread_reply "><span class="ui-icon ui-icon-comment"></span>Reply to This</a>' not in sidebarmenu | |
274 | + assert '<a href="#" class="sidebar_thread_tag "><span class="ui-icon ui-icon-tag"></span>Tag This</a>' not in sidebarmenu | |
275 | + assert '<a href="feed.rss" class=" "><span class="ui-icon ui-icon-signal-diag"></span>Follow This</a>' not in sidebarmenu | |
276 | + assert '<a href="flag_as_spam" class="sidebar_thread_spam "><span class="ui-icon ui-icon-flag"></span>Mark as Spam</a>' not in sidebarmenu | |
277 | + thread = self.app.get('/discussion/TestForum/post', params=dict( | |
278 | + subject='AAA', | |
279 | + text='aaa')).follow() | |
280 | + thread_sidebarmenu = str(thread.html.find('ul',{'id':'sidebarmenu'})) | |
281 | + assert '<a href="#" class="sidebar_thread_reply "><span class="ui-icon ui-icon-comment"></span>Reply to This</a>' in thread_sidebarmenu | |
282 | + assert '<a href="#" class="sidebar_thread_tag "><span class="ui-icon ui-icon-tag"></span>Tag This</a>' in thread_sidebarmenu | |
283 | + assert '<a href="feed.rss" class=" "><span class="ui-icon ui-icon-signal-diag"></span>Follow This</a>' in thread_sidebarmenu | |
284 | + assert '<a href="flag_as_spam" class="sidebar_thread_spam "><span class="ui-icon ui-icon-flag"></span>Mark as Spam</a>' in thread_sidebarmenu | |
261 | 285 | |
262 | 286 | class TestForumAdmin(TestController): |
263 | 287 |
@@ -1,8 +1,9 @@ | ||
1 | 1 | from mimetypes import guess_type |
2 | 2 | |
3 | 3 | from tg import expose, redirect, validate, request, response, flash |
4 | -from tg.decorators import before_validate | |
4 | +from tg.decorators import before_validate, with_trailing_slash, without_trailing_slash | |
5 | 5 | from pylons import c |
6 | +from formencode import validators | |
6 | 7 | from webob import exc |
7 | 8 | |
8 | 9 | from ming.base import Object |
@@ -11,6 +12,7 @@ from ming.utils import LazyProperty | ||
11 | 12 | from pyforge import model as model |
12 | 13 | from pyforge.lib import helpers as h |
13 | 14 | from pyforge.lib.security import require, has_artifact_access |
15 | +from pyforge.lib.helpers import DateTimeConverter | |
14 | 16 | |
15 | 17 | from pyforge.lib.widgets import discuss as DW |
16 | 18 |
@@ -133,11 +135,49 @@ class ThreadController(object): | ||
133 | 135 | @expose() |
134 | 136 | @validate(pass_validator, error_handler=index) |
135 | 137 | def post(self, **kw): |
138 | + require(has_artifact_access('post', self.thread)) | |
136 | 139 | kw = self.W.edit_post.validate(kw, None) |
137 | 140 | p = self.thread.add_post(**kw) |
138 | 141 | flash('Message posted') |
139 | 142 | redirect(request.referer) |
140 | 143 | |
144 | + @expose() | |
145 | + def tag(self, labels, **kw): | |
146 | + require(has_artifact_access('post', self.thread)) | |
147 | + self.thread.labels = labels.split(',') | |
148 | + redirect(request.referer) | |
149 | + | |
150 | + @expose() | |
151 | + def flag_as_spam(self, **kw): | |
152 | + require(has_artifact_access('moderate', self.thread)) | |
153 | + self.thread.first_post.status='spam' | |
154 | + flash('Thread flagged as spam.') | |
155 | + redirect(request.referer) | |
156 | + | |
157 | + @without_trailing_slash | |
158 | + @expose() | |
159 | + @validate(dict( | |
160 | + since=DateTimeConverter(if_empty=None), | |
161 | + until=DateTimeConverter(if_empty=None), | |
162 | + offset=validators.Int(if_empty=None), | |
163 | + limit=validators.Int(if_empty=None))) | |
164 | + def feed(self, since=None, until=None, offset=None, limit=None): | |
165 | + if request.environ['PATH_INFO'].endswith('.atom'): | |
166 | + feed_type = 'atom' | |
167 | + else: | |
168 | + feed_type = 'rss' | |
169 | + title = 'Recent posts to %s' % self.thread.subject | |
170 | + feed = model.Feed.feed( | |
171 | + {'artifact_reference':self.thread.dump_ref()}, | |
172 | + feed_type, | |
173 | + title, | |
174 | + self.thread.url(), | |
175 | + title, | |
176 | + since, until, offset, limit) | |
177 | + response.headers['Content-Type'] = '' | |
178 | + response.content_type = 'application/xml' | |
179 | + return feed.writeString('utf-8') | |
180 | + | |
141 | 181 | class PostController(object): |
142 | 182 | __metaclass__=h.ProxiedAttrMeta |
143 | 183 | M=h.attrproxy('_discussion_controller', 'M') |
@@ -67,6 +67,21 @@ class PostFilter(ew.SimpleForm): | ||
67 | 67 | ]) |
68 | 68 | ] |
69 | 69 | |
70 | +class TagPost(ew.SimpleForm): | |
71 | + | |
72 | + # this ickiness is to override the default submit button | |
73 | + def __call__(self, **kw): | |
74 | + result = super(TagPost, self).__call__(**kw) | |
75 | + submit_button = ffw.SubmitButton(label=result['submit_text']) | |
76 | + result['extra_fields'] = [submit_button] | |
77 | + result['buttons'] = [submit_button] | |
78 | + return result | |
79 | + | |
80 | + fields=[ffw.LabelEdit(label='Tags',name='labels', className='title')] | |
81 | + | |
82 | + def resources(self): | |
83 | + for r in ffw.LabelEdit(name='labels').resources(): yield r | |
84 | + | |
70 | 85 | class EditPost(ew.SimpleForm): |
71 | 86 | show_subject=False |
72 | 87 |
@@ -253,11 +268,12 @@ class Thread(HierWidget): | ||
253 | 268 | pagesize=None |
254 | 269 | total=None |
255 | 270 | show_subject=False |
256 | - new_post_text="New Post" | |
271 | + new_post_text="+ New Comment" | |
257 | 272 | widgets=dict( |
258 | 273 | thread_header=ThreadHeader(), |
259 | 274 | post_thread=PostThread(), |
260 | 275 | post=Post(), |
276 | + tag_post=TagPost(), | |
261 | 277 | edit_post=EditPost(submit_text='Submit')) |
262 | 278 | def resources(self): |
263 | 279 | for r in super(Thread, self).resources(): yield r |
@@ -265,13 +281,50 @@ class Thread(HierWidget): | ||
265 | 281 | for r in w.resources(): |
266 | 282 | yield r |
267 | 283 | yield ew.JSScript(''' |
268 | - $(window).load(function(){ | |
269 | - if($('#new_post_create')){ | |
270 | - $('#new_post_create').click(function(e){ | |
271 | - $(e.target).hide(); | |
272 | - $('#new_post_holder').show(); | |
284 | + $(document).ready(function(){ | |
285 | + var thread_reply = $('a.sidebar_thread_reply'); | |
286 | + var thread_tag = $('a.sidebar_thread_tag'); | |
287 | + var thread_spam = $('a.sidebar_thread_spam'); | |
288 | + var new_post_holder = $('#new_post_holder'); | |
289 | + var new_post_create = $('#new_post_create'); | |
290 | + var tag_thread_holder = $('#tag_thread_holder'); | |
291 | + var allow_moderate = $('#allow_moderate'); | |
292 | + if(new_post_create){ | |
293 | + new_post_create.click(function(e){ | |
294 | + new_post_create.hide(); | |
295 | + new_post_holder.show(); | |
273 | 296 | }); |
274 | 297 | } |
298 | + if(thread_reply){ | |
299 | + if(new_post_holder.length){ | |
300 | + thread_reply[0].style.display='block'; | |
301 | + thread_reply.click(function(e){ | |
302 | + new_post_create.hide(); | |
303 | + new_post_holder.show(); | |
304 | + // focus the submit to scroll to the bottom, then focus the subject for them to start typing | |
305 | + $('input[type="submit"]', new_post_holder).focus(); | |
306 | + $('input[type="text"]', new_post_holder).focus(); | |
307 | + return false; | |
308 | + }); | |
309 | + } | |
310 | + } | |
311 | + if(thread_tag){ | |
312 | + if(tag_thread_holder.length){ | |
313 | + thread_tag[0].style.display='block'; | |
314 | + thread_tag.click(function(e){ | |
315 | + tag_thread_holder.show(); | |
316 | + // focus the submit to scroll to the bottom, then focus the subject for them to start typing | |
317 | + $('input[type="submit"]', tag_thread_holder).focus(); | |
318 | + $('input[type="text"]', tag_thread_holder).focus(); | |
319 | + return false; | |
320 | + }); | |
321 | + } | |
322 | + } | |
323 | + if(thread_spam){ | |
324 | + if(allow_moderate.length){ | |
325 | + thread_spam[0].style.display='block'; | |
326 | + } | |
327 | + } | |
275 | 328 | }); |
276 | 329 | ''') |
277 | 330 |
@@ -15,10 +15,14 @@ | ||
15 | 15 | </py:for> |
16 | 16 | </div> |
17 | 17 | </py:with> |
18 | + <div id="allow_moderate" py:if="has_artifact_access('moderate', value)()"/> | |
18 | 19 | <py:if test="has_artifact_access('post', value)()"> |
19 | 20 | <input id="new_post_create" type="button" class="ui-state-default ui-button ui-button-text" value="$new_post_text"/> |
20 | 21 | <div id="new_post_holder" style="display:none"> |
21 | 22 | ${widgets.edit_post.display(submit_text='New Post', action=value.url() + 'post')} |
22 | 23 | </div> |
24 | + <div id="tag_thread_holder" style="display:none"> | |
25 | + ${widgets.tag_post.display(value=value,submit_text='Tag Post', action=value.url() + 'tag')} | |
26 | + </div> | |
23 | 27 | </py:if> |
24 | 28 | </div> |
@@ -2,7 +2,7 @@ | ||
2 | 2 | xmlns:py="http://genshi.edgewall.org/" |
3 | 3 | xmlns:xi="http://www.w3.org/2001/XInclude"> |
4 | 4 | <xi:include href="${g.pyforge_templates}/lib.html" /> |
5 | - <table id="forum-list" class="forums clear"> | |
5 | + <table id="forum-list" class="clear"> | |
6 | 6 | <thead> |
7 | 7 | <tr> |
8 | 8 | <th> </th> |
@@ -13,7 +13,7 @@ from ming.orm.property import FieldProperty, RelationProperty, ForeignIdProperty | ||
13 | 13 | |
14 | 14 | from pyforge.lib import helpers as h |
15 | 15 | from pyforge.lib.security import require, has_artifact_access |
16 | -from .artifact import Artifact, VersionedArtifact, Snapshot, Message | |
16 | +from .artifact import Artifact, VersionedArtifact, Snapshot, Message, Feed | |
17 | 17 | from .filesystem import File |
18 | 18 | from .types import ArtifactReference, ArtifactReferenceType |
19 | 19 |
@@ -137,6 +137,7 @@ class Thread(Artifact): | ||
137 | 137 | self.num_replies += 1 |
138 | 138 | if not self.first_post: |
139 | 139 | self.first_post_id = p._id |
140 | + Feed.post(self, title=p.subject, description=p.text) | |
140 | 141 | return p |
141 | 142 | |
142 | 143 | def post(self, text, message_id=None, parent_id=None, **kw): |
@@ -688,7 +688,7 @@ border-bottom: 4px solid rgb(215,215,215);} | ||
688 | 688 | .tagEditor li |
689 | 689 | { |
690 | 690 | display: inline; |
691 | - background-image: url(https://newforge.sf.geek.net/images/minus_small.png); | |
691 | + background-image: url(/images/minus_small.png); | |
692 | 692 | background-color: #eef; |
693 | 693 | background-position: right center; |
694 | 694 | background-repeat: no-repeat; |
@@ -832,3 +832,6 @@ small.badge:hover { | ||
832 | 832 | |
833 | 833 | #tipcca { text-align: left; height:3em; padding-left: 55px; width: 190px; background-image: url('images/cca.png'); background-position: 5px 50%; background-repeat: no-repeat;} |
834 | 834 | |
835 | +ul#sidebarmenu li a.sidebar_thread_tag, | |
836 | +ul#sidebarmenu li a.sidebar_thread_reply, | |
837 | +ul#sidebarmenu li a.sidebar_thread_spam{display:none;} | |
\ No newline at end of file |