Merge branch 'dev' into t48_blog_pull_in_rss

Conflicts:
	Allura/allura/command/__init__.py
	Allura/allura/tests/test_commands.py
	Allura/setup.py
diff --git a/Allura/allura/command/__init__.py b/Allura/allura/command/__init__.py
index 88ddebc..0f7d0a7 100644
--- a/Allura/allura/command/__init__.py
+++ b/Allura/allura/command/__init__.py
@@ -4,4 +4,5 @@
 from smtp_server import SMTPServerCommand
 from create_neighborhood import CreateNeighborhoodCommand
 from create_trove_categories import CreateTroveCategoriesCommand
+from rssfeeds import RssFeedsCommand
 from set_neighborhood_features import SetNeighborhoodFeaturesCommand
diff --git a/Allura/allura/command/rssfeeds.py b/Allura/allura/command/rssfeeds.py
new file mode 100644
index 0000000..335ede9
--- /dev/null
+++ b/Allura/allura/command/rssfeeds.py
@@ -0,0 +1,84 @@
+import feedparser
+import html2text
+from bson import ObjectId
+
+import base
+
+from pylons import c
+
+from allura import model as M
+from forgeblog import model as BM
+from forgeblog import version
+from forgeblog.main import ForgeBlogApp 
+from allura.lib import exceptions
+
+
+class RssFeedsCommand(base.Command):
+    summary = 'Rss feed client'
+    parser = base.Command.standard_parser(verbose=True)
+    parser.add_option('-a', '--appid', dest='appid', default='',
+                      help='application id')
+    parser.add_option('-u', '--username', dest='username', default='root',
+                      help='poster username')
+
+    def command(self):
+        self.basic_setup()
+
+        user = M.User.query.get(username=self.options.username)
+        c.user = user
+
+        self.prepare_feeds()
+        for appid in self.feed_dict:
+            for feed_url in self.feed_dict[appid]:
+                self.process_feed(appid, feed_url)
+
+    def prepare_feeds(self):
+        feed_dict = {}
+        if self.options.appid != '':
+            gl_app = BM.Globals.query.get(app_config_id=ObjectId(self.options.appid))
+            if not gl_app:
+                raise exceptions.NoSuchGlobalsError("The globals %s " \
+                     "could not be found in the database" % self.options.appid)
+            if len(gl_app.external_feeds) > 0:
+                feed_dict[gl_app.app_config_id] = gl_app.external_feeds
+        else:
+            for gl_app in BM.Globals.query.find().all():
+                if len(gl_app.external_feeds) > 0:
+                    feed_dict[gl_app.app_config_id] = gl_app.external_feeds
+        self.feed_dict = feed_dict
+
+    def process_feed(self, appid, feed_url):
+        appconf = M.AppConfig.query.get(_id=appid)
+        if not appconf:
+            return
+
+        c.project = appconf.project
+        app = ForgeBlogApp(c.project, appconf)
+        c.app = app
+
+        base.log.info("Get feed: %s" % feed_url)
+        f = feedparser.parse(feed_url)
+        if f.bozo:
+            base.log.exception("%s: %s" % (feed_url, f.bozo_exception))
+            return
+        for e in f.entries:
+            title = e.title
+            if 'content' in e:
+                content = u''
+                for ct in e.content:
+                    if ct.type != 'text/html':
+                        content = u"%s<p>%s</p>" % (content, ct.value)
+                    else:
+                        content = content + ct.value
+            else:
+                content = e.summary
+
+            content = u'%s <a href="%s">link</a>' % (content, e.link)
+            content = html2text.html2text(content, e.link)
+
+            post = BM.BlogPost(title=title, text=content, app_config_id=appid,
+                               tool_version={'blog': version.__version__},
+                               state='draft')
+            post.neighborhood_id=c.project.neighborhood_id
+            post.make_slug()
+            post.commit()
diff --git a/Allura/allura/lib/exceptions.py b/Allura/allura/lib/exceptions.py
index fa9f4a0..30250f7 100644
--- a/Allura/allura/lib/exceptions.py
+++ b/Allura/allura/lib/exceptions.py
@@ -4,6 +4,7 @@
 class ToolError(ForgeError): pass
 class NoSuchProjectError(ForgeError): pass
 class NoSuchNeighborhoodError(ForgeError): pass
+class NoSuchGlobalsError(ForgeError): pass
 class MailError(ForgeError): pass
 class AddressException(MailError): pass
 class NoSuchNBFeatureError(ForgeError): pass
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index 6bcdb8a..6d2ef99 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -1,26 +1,32 @@
 from nose.tools import assert_raises
 
+from ming.orm.ormsession import ThreadLocalORMSession
+
 from alluratest.controller import setup_basic_test, setup_global_objects
-from allura.command import script, set_neighborhood_features
+from allura.command import script, set_neighborhood_features, rssfeeds
 from allura import model as M
 from allura.lib.exceptions import InvalidNBFeatureValueError
+from forgeblog import model as BM
 
 
 test_config = 'test.ini#main'
 
 class EmptyClass(object): pass
 
+
 def setUp(self):
     """Method called by nose before running each test"""
     #setup_basic_test(app_name='main_with_amqp')
     setup_basic_test()
     setup_global_objects()
 
+
 def test_script():
     cmd = script.ScriptCommand('script')
-    cmd.run([test_config, 'allura/tests/tscript.py' ])
+    cmd.run([test_config, 'allura/tests/tscript.py'])
     cmd.command()
-    assert_raises(ValueError, cmd.run, [test_config, 'allura/tests/tscript_error.py' ])
+    assert_raises(ValueError, cmd.run, [test_config, 'allura/tests/tscript_error.py'])
+
 
 def test_set_neighborhood_max_projects():
     neighborhood = M.Neighborhood.query.find().first()
@@ -43,6 +49,7 @@
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'max_projects', 'string'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'max_projects', '2.8'])
 
+
 def test_set_neighborhood_private():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -65,6 +72,7 @@
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'private_projects', '1'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'private_projects', '2.8'])
 
+
 def test_set_neighborhood_google_analytics():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -80,6 +88,7 @@
     cmd.run([test_config, str(n_id), 'google_analytics', 'False'])
     cmd.command()
     neighborhood = M.Neighborhood.query.get(_id=n_id)
+
     assert not neighborhood.features['google_analytics']
 
     # check validation
@@ -87,6 +96,7 @@
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'google_analytics', '1'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'google_analytics', '2.8'])
 
+
 def test_set_neighborhood_css():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -116,3 +126,22 @@
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', '2.8'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'None'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'True'])
+
+
+def test_pull_rss_feeds():
+    base_app = M.AppConfig.query.find().all()[0]
+    tmp_app = M.AppConfig(tool_name=u'Blog', discussion_id=base_app.discussion_id,
+                          project_id=base_app.project_id,
+                          options={u'ordinal': 0, u'show_right_bar': True,
+                                    u'project_name': base_app.project.name,
+                                    u'mount_point': u'blog',
+                                    u'mount_label': u'Blog'})
+    new_external_feeds = ['http://wordpress.org/news/feed/']
+    BM.Globals(app_config_id=tmp_app._id, external_feeds=new_external_feeds)
+    ThreadLocalORMSession.flush_all()
+
+    cmd = rssfeeds.RssFeedsCommand('pull-rss-feeds')
+    cmd.run([test_config, '-a', tmp_app._id])
+    cmd.command()
+
+    assert len(BM.BlogPost.query.find({'app_config_id': tmp_app._id}).all()) > 0
diff --git a/Allura/setup.py b/Allura/setup.py
index 6420acd..9b4f5a0 100644
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -113,6 +113,7 @@
     create-neighborhood = allura.command:CreateNeighborhoodCommand
     create-trove-categories = allura.command:CreateTroveCategoriesCommand
     set-neighborhood-features = allura.command:SetNeighborhoodFeaturesCommand
+    pull-rss-feeds = allura.command.rssfeeds:RssFeedsCommand
 
     [easy_widgets.resources]
     ew_resources=allura.config.resources:register_ew_resources
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index bacc532..f7e1d0a 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -12,12 +12,14 @@
 from formencode import validators
 from webob import exc
 
+from ming.orm import session
+
 # Pyforge-specific imports
 from allura.app import Application, ConfigOption, SitemapEntry
 from allura.app import DefaultAdminController
 from allura.lib import helpers as h
 from allura.lib.search import search
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, Property
 from allura.lib.security import has_access, require_access
 from allura.lib import widgets as w
 from allura.lib.widgets.subscriptions import SubscribeForm
@@ -56,6 +58,7 @@
     ordinal=14
     installable=True
     config_options = Application.config_options
+    default_external_feeds = []
     icons={
         24:'images/blog_24.png',
         32:'images/blog_32.png',
@@ -67,6 +70,24 @@
         self.root = RootController()
         self.admin = BlogAdminController(self)
 
+    @Property
+    def external_feeds_list():
+        def fget(self):
+            globals = BM.Globals.query.get(app_config_id=self.config._id)
+            if globals is not None:
+                external_feeds = globals.external_feeds
+            else:
+                external_feeds = self.default_external_feeds
+            return external_feeds
+        def fset(self, new_external_feeds):
+            globals = BM.Globals.query.get(app_config_id=self.config._id)
+            if globals is not None:
+                globals.external_feeds = new_external_feeds
+            elif len(new_external_feeds) > 0:
+                globals = BM.Globals(app_config_id=self.config._id, external_feeds=new_external_feeds)
+            if globals is not None:
+                session(globals).flush()
+
     @property
     @h.exceptionless([], log)
     def sitemap(self):
@@ -94,7 +115,11 @@
         return links
 
     def admin_menu(self):
-        return super(ForgeBlogApp, self).admin_menu(force_options=True)
+        admin_url = c.project.url() + 'admin/' + self.config.options.mount_point + '/'
+        links = [SitemapEntry('External feeds', admin_url + 'exfeed', className='admin_modal')]
+        links += super(ForgeBlogApp, self).admin_menu(force_options=True)
+        return links
+        #return super(ForgeBlogApp, self).admin_menu(force_options=True)
 
     def install(self, project):
         'Set up any default permissions and roles here'
@@ -359,3 +384,34 @@
         self.app.config.options['show_discussion'] = show_discussion and True or False
         flash('Blog options updated')
         redirect(h.really_unicode(c.project.url()+'admin/tools').encode('utf-8'))
+
+    @without_trailing_slash
+    @expose('jinja:forgeblog:templates/blog/admin_exfeed.html')
+    def exfeed(self):
+        #self.app.external_feeds_list = ['feed1', 'feed2']
+        #log.info("EXFEED: %s" % self.app.external_feeds_list)
+        feeds_list = []
+        for feed in self.app.external_feeds_list:
+            feeds_list.append(feed)
+        return dict(app=self.app,
+                    feeds_list=feeds_list,
+                    allow_config=has_access(self.app, 'configure')())
+
+    @without_trailing_slash
+    @expose()
+    @require_post()
+    def set_exfeed(self, **kw):
+        new_exfeed = kw.get('new_exfeed', None)
+        exfeed_val = kw.get('exfeed', [])
+        if type(exfeed_val) == unicode:
+            exfeed_list = []
+            exfeed_list.append(exfeed_val)
+        else:
+            exfeed_list = exfeed_val
+
+        if new_exfeed is not None and new_exfeed != '':
+            exfeed_list.append(new_exfeed)
+
+        self.app.external_feeds_list = exfeed_list
+        flash('External feeds updated')
+        redirect(c.project.url()+'admin/tools')
diff --git a/ForgeBlog/forgeblog/model/__init__.py b/ForgeBlog/forgeblog/model/__init__.py
index 3f6c73e..3a7399f 100644
--- a/ForgeBlog/forgeblog/model/__init__.py
+++ b/ForgeBlog/forgeblog/model/__init__.py
@@ -1 +1 @@
-from blog import BlogPost, Attachment, BlogPostSnapshot
+from blog import Globals, BlogPost, Attachment, BlogPostSnapshot
diff --git a/ForgeBlog/forgeblog/model/blog.py b/ForgeBlog/forgeblog/model/blog.py
index 938b408..0909108 100644
--- a/ForgeBlog/forgeblog/model/blog.py
+++ b/ForgeBlog/forgeblog/model/blog.py
@@ -9,6 +9,8 @@
 
 from ming import schema
 from ming.orm import FieldProperty, ForeignIdProperty, Mapper, session, state
+from ming.orm.declarative import MappedClass
+
 from allura import model as M
 from allura.lib import helpers as h
 from allura.lib import utils, patience
@@ -16,6 +18,19 @@
 config = utils.ConfigProxy(
     common_suffix='forgemail.domain')
 
+class Globals(MappedClass):
+
+    class __mongometa__:
+        name = 'blog-globals'
+        session = M.project_orm_session
+        indexes = [ 'app_config_id' ]
+
+    type_s = 'BlogGlobals'
+    _id = FieldProperty(schema.ObjectId)
+    app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
+    external_feeds=FieldProperty([str])
+
+
 class BlogPostSnapshot(M.Snapshot):
     class __mongometa__:
         name='blog_post_snapshot'
diff --git a/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html b/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html
new file mode 100644
index 0000000..2c337bc
--- /dev/null
+++ b/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html
@@ -0,0 +1,27 @@
+<form method="POST" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/set_exfeed">
+  <label class="grid-13">Existing external feeds:</label>
+  <div class="grid-13">
+    <ul>
+    {% if allow_config %}
+     {% for feed in feeds_list %}
+     <li><input type="checkbox" name="exfeed" value="{{ feed }}" checked="checked"><span>{{ feed }}</span></li>
+     {% endfor %} 
+    {% else %}
+     {% for feed in feeds_list %}
+     <li><span>{{ feed.value }}</span></li>
+     {% endfor %} 
+    {% endif %}
+  </div>
+  {% if allow_config %}
+    <div class="grid-13">&nbsp;</div>
+    <div class="grid-13">
+       <input type="text" name="new_exfeed" id="new_exfeed" value=""/>
+    </div>
+    <div class="grid-13">&nbsp;</div>
+    <hr>
+    <div class="grid-13">&nbsp;</div>
+    <div class="grid-13">
+      <input type="submit" value="Save"/>
+    </div>
+  {% endif %}
+</form>
diff --git a/requirements-common.txt b/requirements-common.txt
index c6b1a0f..57157c1 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -15,6 +15,7 @@
 # dep of Creoleparser
 Genshi==0.6
 # dep of oauth2
+html2text==3.200.3
 httplib2==0.7.4
 iso8601==0.1.4
 Jinja2==2.6