• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#objective-cqt誰得windowscocoapythonphprubygameguibathyscaphec翻訳omegat計画中(planning stage)frameworktwittertestdomvb.netdirectxbtronarduinopreviewerゲームエンジン

allura


Commit MetaInfo

Revisión70fa32ad2e434d4bb7f3a130da33da039a469976 (tree)
Tiempo2012-05-22 05:08:10
AutorYaroslav Luzin <jardev@gmai...>
CommiterYaroslav Luzin

Log Message

Merge branch 'dev' into t50_hide_project_icon_placeholder

Cambiar Resumen

Diferencia incremental

--- a/Allura/allura/command/__init__.py
+++ b/Allura/allura/command/__init__.py
@@ -5,3 +5,4 @@ from smtp_server import SMTPServerCommand
55 from create_neighborhood import CreateNeighborhoodCommand
66 from create_trove_categories import CreateTroveCategoriesCommand
77 from set_neighborhood_features import SetNeighborhoodFeaturesCommand
8+from rssfeeds import RssFeedsCommand
--- /dev/null
+++ b/Allura/allura/command/rssfeeds.py
@@ -0,0 +1,84 @@
1+import feedparser
2+import html2text
3+from bson import ObjectId
4+
5+import base
6+
7+from pylons import c
8+
9+from allura import model as M
10+from forgeblog import model as BM
11+from forgeblog import version
12+from forgeblog.main import ForgeBlogApp
13+from allura.lib import exceptions
14+
15+
16+class RssFeedsCommand(base.Command):
17+ summary = 'Rss feed client'
18+ parser = base.Command.standard_parser(verbose=True)
19+ parser.add_option('-a', '--appid', dest='appid', default='',
20+ help='application id')
21+ parser.add_option('-u', '--username', dest='username', default='root',
22+ help='poster username')
23+
24+ def command(self):
25+ self.basic_setup()
26+
27+ user = M.User.query.get(username=self.options.username)
28+ c.user = user
29+
30+ self.prepare_feeds()
31+ for appid in self.feed_dict:
32+ for feed_url in self.feed_dict[appid]:
33+ self.process_feed(appid, feed_url)
34+
35+ def prepare_feeds(self):
36+ feed_dict = {}
37+ if self.options.appid != '':
38+ gl_app = BM.Globals.query.get(app_config_id=ObjectId(self.options.appid))
39+ if not gl_app:
40+ raise exceptions.NoSuchGlobalsError("The globals %s " \
41+ "could not be found in the database" % self.options.appid)
42+ if len(gl_app.external_feeds) > 0:
43+ feed_dict[gl_app.app_config_id] = gl_app.external_feeds
44+ else:
45+ for gl_app in BM.Globals.query.find().all():
46+ if len(gl_app.external_feeds) > 0:
47+ feed_dict[gl_app.app_config_id] = gl_app.external_feeds
48+ self.feed_dict = feed_dict
49+
50+ def process_feed(self, appid, feed_url):
51+ appconf = M.AppConfig.query.get(_id=appid)
52+ if not appconf:
53+ return
54+
55+ c.project = appconf.project
56+ app = ForgeBlogApp(c.project, appconf)
57+ c.app = app
58+
59+ base.log.info("Get feed: %s" % feed_url)
60+ f = feedparser.parse(feed_url)
61+ if f.bozo:
62+ base.log.exception("%s: %s" % (feed_url, f.bozo_exception))
63+ return
64+ for e in f.entries:
65+ title = e.title
66+ if 'content' in e:
67+ content = u''
68+ for ct in e.content:
69+ if ct.type != 'text/html':
70+ content = u"%s<p>%s</p>" % (content, ct.value)
71+ else:
72+ content = content + ct.value
73+ else:
74+ content = e.summary
75+
76+ content = u'%s <a href="%s">link</a>' % (content, e.link)
77+ content = html2text.html2text(content, e.link)
78+
79+ post = BM.BlogPost(title=title, text=content, app_config_id=appid,
80+ tool_version={'blog': version.__version__},
81+ state='draft')
82+ post.neighborhood_id=c.project.neighborhood_id
83+ post.make_slug()
84+ post.commit()
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -23,7 +23,7 @@ from ming.orm.middleware import MingMiddleware
2323 from allura.config.app_cfg import base_config
2424 from allura.config.environment import load_environment
2525 from allura.config.app_cfg import ForgeConfig
26-from allura.lib.custom_middleware import StatsMiddleware
26+from allura.lib.custom_middleware import AlluraTimerMiddleware
2727 from allura.lib.custom_middleware import SSLMiddleware
2828 from allura.lib.custom_middleware import StaticFilesMiddleware
2929 from allura.lib.custom_middleware import CSRFMiddleware
@@ -33,8 +33,8 @@ from allura.lib import helpers as h
3333
3434 __all__ = ['make_app']
3535
36-# Use base_config to setup the necessary PasteDeploy application factory.
37-# make_base_app will wrap the TG2 app with all the middleware it needs.
36+# Use base_config to setup the necessary PasteDeploy application factory.
37+# make_base_app will wrap the TG2 app with all the middleware it needs.
3838 make_base_app = base_config.setup_tg_wsgi_app(load_environment)
3939
4040
@@ -55,13 +55,13 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
5555 :type full_stack: str or bool
5656 :return: The allura application with all the relevant middleware
5757 loaded.
58-
58+
5959 This is the PasteDeploy factory for the allura application.
60-
60+
6161 ``app_conf`` contains all the application-specific settings (those defined
6262 under ``[app:main]``.
63-
64-
63+
64+
6565 """
6666 # Run all the initialization code here
6767 mimetypes.init(
@@ -73,7 +73,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
7373
7474 # Configure EW variable provider
7575 ew.render.TemplateEngine.register_variable_provider(get_tg_vars)
76-
76+
7777 # Create base app
7878 base_config = ForgeConfig(root)
7979 load_environment = base_config.make_load_environment()
@@ -86,7 +86,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
8686 load_environment(global_conf, app_conf)
8787
8888 if config.get('zarkov.host'):
89- try:
89+ try:
9090 import zmq
9191 except ImportError:
9292 raise ImportError, "Unable to import the zmq library. Please"\
@@ -113,9 +113,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
113113 # Redirect 401 to the login page
114114 app = LoginRedirectMiddleware(app)
115115 # Add instrumentation
116- if app_conf.get('stats.sample_rate', '0.25') != '0':
117- stats_config = dict(global_conf, **app_conf)
118- app = StatsMiddleware(app, stats_config)
116+ app = AlluraTimerMiddleware(app, app_conf)
119117 # Clear cookies when the CSRF field isn't posted
120118 if not app_conf.get('disable_csrf_protection'):
121119 app = CSRFMiddleware(app, '_session_id')
@@ -145,7 +143,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
145143 # the WSGI application's iterator is exhausted
146144 app = RegistryManager(app, streaming=True)
147145 return app
148-
146+
149147 def set_scheme_middleware(app):
150148 def SchemeMiddleware(environ, start_response):
151149 if asbool(environ.get('HTTP_X_SFINC_SSL', 'false')):
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -1,6 +1,8 @@
11 import os
22 import json
33 import logging
4+import re
5+import difflib
46 from urllib import quote, unquote
57 from collections import defaultdict
68
@@ -15,7 +17,6 @@ from ming.base import Object
1517 from ming.orm import ThreadLocalORMSession, session
1618
1719 import allura.tasks
18-from allura.lib import patience
1920 from allura.lib import security
2021 from allura.lib import helpers as h
2122 from allura.lib import widgets as w
@@ -61,7 +62,7 @@ class RepoRootController(BaseController):
6162
6263 @with_trailing_slash
6364 @expose('jinja:allura:templates/repo/fork.html')
64- def fork(self, to_name=None, project_id=None):
65+ def fork(self, project_id=None, mount_point=None, mount_label=None):
6566 # this shows the form and handles the submission
6667 security.require_authenticated()
6768 if not c.app.forkable: raise exc.HTTPNotFound
@@ -70,10 +71,13 @@ class RepoRootController(BaseController):
7071 ThreadLocalORMSession.close_all()
7172 from_project = c.project
7273 to_project = M.Project.query.get(_id=ObjectId(project_id))
73- if request.method != 'POST' or not to_name:
74+ mount_label = mount_label or '%s - %s' % (c.project.name, from_repo.tool_name)
75+ mount_point = (mount_point or from_project.shortname)
76+ if request.method != 'POST' or not mount_point:
7477 return dict(from_repo=from_repo,
7578 user_project=c.user.private_project(),
76- to_name=to_name or '')
79+ mount_point=mount_point,
80+ mount_label=mount_label)
7781 else:
7882 with h.push_config(c, project=to_project):
7983 if not to_project.database_configured:
@@ -81,10 +85,12 @@ class RepoRootController(BaseController):
8185 security.require(security.has_access(to_project, 'admin'))
8286 try:
8387 to_project.install_app(
84- from_repo.tool_name, to_name,
88+ ep_name=from_repo.tool_name,
89+ mount_point=mount_point,
90+ mount_label=mount_label,
8591 cloned_from_project_id=from_project._id,
8692 cloned_from_repo_id=from_repo._id)
87- redirect(to_project.url()+to_name+'/')
93+ redirect(to_project.url()+mount_point+'/')
8894 except exc.HTTPRedirection:
8995 raise
9096 except Exception, ex:
@@ -514,7 +520,7 @@ class FileBrowser(BaseController):
514520 b = self._blob
515521 la = list(a)
516522 lb = list(b)
517- diff = ''.join(patience.unified_diff(
523+ diff = ''.join(difflib.unified_diff(
518524 la, lb,
519525 ('a' + apath).encode('utf-8'),
520526 ('b' + b.path()).encode('utf-8')))
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -2,7 +2,7 @@
22
33 {% block title %}{{user.display_name}} / Profile{% endblock %}
44
5-{% block header %}{{c.project.homepage_title}}{% endblock %}
5+{% block header %}{{ user.display_name|default(user.username) }}{% endblock %}
66
77 {% block extra_css %}
88 <link rel="stylesheet" type="text/css"
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -60,7 +60,7 @@ class Globals(object):
6060 if asbool(config.get('solr.mock')):
6161 self.solr = MockSOLR()
6262 elif self.solr_server:
63- self.solr = pysolr.Solr(self.solr_server)
63+ self.solr = pysolr.Solr(self.solr_server)
6464 else: # pragma no cover
6565 self.solr = None
6666 self.use_queue = asbool(config.get('use_queue', False))
@@ -77,7 +77,7 @@ class Globals(object):
7777 # Setup pygments
7878 self.pygments_formatter = utils.LineAnchorCodeHtmlFormatter(
7979 cssclass='codehilite',
80- linenos='inline')
80+ linenos='table')
8181
8282 # Setup Pypeline
8383 self.pypeline_markup = pypeline_markup
@@ -86,42 +86,42 @@ class Globals(object):
8686 self.analytics = analytics.GoogleAnalytics(account=config.get('ga.account', 'UA-XXXXX-X'))
8787
8888 self.icons = dict(
89- admin = Icon('x', 'ico-admin'),
90- pencil = Icon('p', 'ico-pencil'),
91- help = Icon('h', 'ico-help'),
92- search = Icon('s', 'ico-search'),
93- history = Icon('N', 'ico-history'),
94- feed = Icon('f', 'ico-feed'),
95- mail = Icon('M', 'ico-mail'),
96- reply = Icon('w', 'ico-reply'),
97- tag = Icon('z', 'ico-tag'),
98- flag = Icon('^', 'ico-flag'),
99- undelete = Icon('+', 'ico-undelete'),
100- delete = Icon('#', 'ico-delete'),
101- close = Icon('D', 'ico-close'),
102- table = Icon('n', 'ico-table'),
103- stats = Icon('Y', 'ico-stats'),
104- pin = Icon('@', 'ico-pin'),
105- folder = Icon('o', 'ico-folder'),
106- fork = Icon('R', 'ico-fork'),
107- merge = Icon('J', 'ico-merge'),
108- plus = Icon('+', 'ico-plus'),
109- conversation = Icon('q', 'ico-conversation'),
110- group = Icon('g', 'ico-group'),
111- user = Icon('U', 'ico-user'),
112- secure = Icon('(', 'ico-lock'),
113- unsecure = Icon(')', 'ico-unlock'),
89+ admin=Icon('x', 'ico-admin'),
90+ pencil=Icon('p', 'ico-pencil'),
91+ help=Icon('h', 'ico-help'),
92+ search=Icon('s', 'ico-search'),
93+ history=Icon('N', 'ico-history'),
94+ feed=Icon('f', 'ico-feed'),
95+ mail=Icon('M', 'ico-mail'),
96+ reply=Icon('w', 'ico-reply'),
97+ tag=Icon('z', 'ico-tag'),
98+ flag=Icon('^', 'ico-flag'),
99+ undelete=Icon('+', 'ico-undelete'),
100+ delete=Icon('#', 'ico-delete'),
101+ close=Icon('D', 'ico-close'),
102+ table=Icon('n', 'ico-table'),
103+ stats=Icon('Y', 'ico-stats'),
104+ pin=Icon('@', 'ico-pin'),
105+ folder=Icon('o', 'ico-folder'),
106+ fork=Icon('R', 'ico-fork'),
107+ merge=Icon('J', 'ico-merge'),
108+ plus=Icon('+', 'ico-plus'),
109+ conversation=Icon('q', 'ico-conversation'),
110+ group=Icon('g', 'ico-group'),
111+ user=Icon('U', 'ico-user'),
112+ secure=Icon('(', 'ico-lock'),
113+ unsecure=Icon(')', 'ico-unlock'),
114114 # Permissions
115- perm_read = Icon('E', 'ico-focus'),
116- perm_update = Icon('0', 'ico-sync'),
117- perm_create = Icon('e', 'ico-config'),
118- perm_register = Icon('e', 'ico-config'),
119- perm_delete = Icon('-', 'ico-minuscirc'),
120- perm_tool = Icon('x', 'ico-config'),
121- perm_admin = Icon('(', 'ico-lock'),
122- perm_has_yes = Icon('3', 'ico-check'),
123- perm_has_no = Icon('d', 'ico-noentry'),
124- perm_has_inherit = Icon('2', 'ico-checkcircle'),
115+ perm_read=Icon('E', 'ico-focus'),
116+ perm_update=Icon('0', 'ico-sync'),
117+ perm_create=Icon('e', 'ico-config'),
118+ perm_register=Icon('e', 'ico-config'),
119+ perm_delete=Icon('-', 'ico-minuscirc'),
120+ perm_tool=Icon('x', 'ico-config'),
121+ perm_admin=Icon('(', 'ico-lock'),
122+ perm_has_yes=Icon('3', 'ico-check'),
123+ perm_has_no=Icon('d', 'ico-noentry'),
124+ perm_has_inherit=Icon('2', 'ico-checkcircle'),
125125 )
126126
127127 # Cache some loaded entry points
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -1,21 +1,14 @@
11 import os
22 import re
33 import logging
4-from contextlib import contextmanager
5-from threading import local
6-from random import random
74
85 import tg
9-import pylons
106 import pkg_resources
11-import markdown
127 from paste import fileapp
13-from paste.deploy.converters import asbool
148 from pylons.util import call_wsgi_application
15-from tg.controllers import DecoratedController
9+from timermiddleware import Timer, TimerMiddleware
1610 from webob import exc, Request
1711
18-from allura.lib.stats import timing, StatsRecord
1912 from allura.lib import helpers as h
2013
2114 log = logging.getLogger(__name__)
@@ -151,57 +144,39 @@ class SSLMiddleware(object):
151144 resp = req.get_response(self.app)
152145 return resp(environ, start_response)
153146
154-class StatsMiddleware(object):
155-
156- def __init__(self, app, config):
157- self.app = app
158- self.config = config
159- self.log = logging.getLogger('stats')
160- self.active = False
161- try:
162- self.sample_rate = config.get('stats.sample_rate', 0.25)
163- self.debug = asbool(config.get('debug', 'false'))
164- self.instrument_pymongo()
165- self.instrument_template()
166- self.active = True
167- except KeyError:
168- self.sample_rate = 0
169-
170- def instrument_pymongo(self):
171- import pymongo.collection
172- import ming.odm
173- timing('mongo').decorate(pymongo.collection.Collection,
174- 'count find find_one')
175- timing('mongo').decorate(pymongo.cursor.Cursor,
176- 'count distinct explain hint limit next rewind'
177- ' skip sort where')
178- timing('ming').decorate(ming.odm.odmsession.ODMSession,
179- 'flush find get')
180- timing('ming').decorate(ming.odm.odmsession.ODMCursor,
181- 'next')
182-
183- def instrument_template(self):
147+class AlluraTimerMiddleware(TimerMiddleware):
148+ def timers(self):
149+ import genshi
184150 import jinja2
185- import genshi.template
186- timing('template').decorate(genshi.template.Template,
187- '_prepare _parse generate')
188- timing('render').decorate(genshi.Stream,
189- 'render')
190- timing('render').decorate(jinja2.Template,
191- 'render')
192- timing('markdown').decorate(markdown.Markdown,
193- 'convert')
194-
195-
196- def __call__(self, environ, start_response):
197- req = Request(environ)
198- req.environ['sf.stats'] = s = StatsRecord(req, random() < self.sample_rate)
199- with s.timing('total'):
200- resp = req.get_response(self.app, catch_exc_info=self.debug)
201- result = resp(environ, start_response)
202- if s.active:
203- self.log.info('Stats: %r', s)
204- from allura import model as M
205- M.Stats.make(s.asdict()).m.insert()
206- return result
207-
151+ import markdown
152+ import ming
153+ import pymongo
154+ import socket
155+ import urllib2
156+
157+ return [
158+ Timer('markdown', markdown.Markdown, 'convert'),
159+ Timer('ming', ming.odm.odmsession.ODMCursor, 'next'),
160+ Timer('ming', ming.odm.odmsession.ODMSession, 'flush', 'find',
161+ 'get'),
162+ Timer('ming', ming.schema.Document, 'validate',
163+ debug_each_call=False),
164+ Timer('ming', ming.schema.FancySchemaItem, '_validate_required',
165+ '_validate_fast_missing', '_validate_optional',
166+ debug_each_call=False),
167+ Timer('mongo', pymongo.collection.Collection, 'count', 'find',
168+ 'find_one'),
169+ Timer('mongo', pymongo.cursor.Cursor, 'count', 'distinct',
170+ 'explain', 'hint', 'limit', 'next', 'rewind', 'skip',
171+ 'sort', 'where'),
172+ Timer('jinja', jinja2.Template, 'render', 'stream', 'generate'),
173+ # urlopen and socket io may or may not overlap partially
174+ Timer('urlopen', urllib2, 'urlopen'),
175+ Timer('render', genshi.Stream, 'render'),
176+ Timer('socket_read', socket._fileobject, 'read', 'readline',
177+ 'readlines', debug_each_call=False),
178+ Timer('socket_write', socket._fileobject, 'write', 'writelines',
179+ 'flush', debug_each_call=False),
180+ Timer('template', genshi.template.Template, '_prepare', '_parse',
181+ 'generate'),
182+ ]
--- a/Allura/allura/lib/exceptions.py
+++ b/Allura/allura/lib/exceptions.py
@@ -4,6 +4,7 @@ class ProjectOverlimitError(ForgeError): pass
44 class ToolError(ForgeError): pass
55 class NoSuchProjectError(ForgeError): pass
66 class NoSuchNeighborhoodError(ForgeError): pass
7+class NoSuchGlobalsError(ForgeError): pass
78 class MailError(ForgeError): pass
89 class AddressException(MailError): pass
910 class NoSuchNBFeatureError(ForgeError): pass
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -142,8 +142,8 @@ def _make_xs(X, ids):
142142 results = dict(
143143 (r._id, r)
144144 for r in X.query.find(dict(_id={'$in':ids})))
145- result = ( results.get(i) for i in ids )
146- return ( r for r in result if r is not None )
145+ result = (results.get(i) for i in ids)
146+ return (r for r in result if r is not None)
147147
148148 @contextmanager
149149 def push_config(obj, **kw):
@@ -158,14 +158,14 @@ def push_config(obj, **kw):
158158 try:
159159 yield obj
160160 finally:
161- for k,v in saved_attrs.iteritems():
161+ for k, v in saved_attrs.iteritems():
162162 setattr(obj, k, v)
163163 for k in new_attrs:
164164 delattr(obj, k)
165165
166166 def sharded_path(name, num_parts=2):
167167 parts = [
168- name[:i+1]
168+ name[:i + 1]
169169 for i in range(num_parts) ]
170170 return '/'.join(parts)
171171
@@ -227,12 +227,12 @@ def encode_keys(d):
227227 a valid kwargs argument'''
228228 return dict(
229229 (k.encode('utf-8'), v)
230- for k,v in d.iteritems())
230+ for k, v in d.iteritems())
231231
232232 def vardec(fun):
233233 def vardec_hook(remainder, params):
234234 new_params = variable_decode(dict(
235- (k,v) for k,v in params.items()
235+ (k, v) for k, v in params.items()
236236 if re_clean_vardec_key.match(k)))
237237 params.update(new_params)
238238 before_validate(vardec_hook)(fun)
@@ -276,7 +276,7 @@ class DateTimeConverter(FancyValidator):
276276 try:
277277 return parse(value)
278278 except ValueError:
279- if self.if_invalid!=formencode.api.NoDefault:
279+ if self.if_invalid != formencode.api.NoDefault:
280280 return self.if_invalid
281281 else:
282282 raise
@@ -336,7 +336,7 @@ class ProxiedAttrMeta(type):
336336 v.cls = cls
337337
338338 class attrproxy(object):
339- cls=None
339+ cls = None
340340 def __init__(self, *attrs):
341341 self.attrs = attrs
342342
@@ -413,7 +413,7 @@ def json_validation_error(controller, **kwargs):
413413 errors=c.validation_exception.unpack_errors(),
414414 value=c.validation_exception.value,
415415 params=kwargs)
416- response.status=400
416+ response.status = 400
417417 return json.dumps(result, indent=2)
418418
419419 def pop_user_notifications(user=None):
@@ -429,8 +429,8 @@ def config_with_prefix(d, prefix):
429429 '''Return a subdictionary keys with a given prefix,
430430 with the prefix stripped
431431 '''
432- plen=len(prefix)
433- return dict((k[plen:], v) for k,v in d.iteritems()
432+ plen = len(prefix)
433+ return dict((k[plen:], v) for k, v in d.iteritems()
434434 if k.startswith(prefix))
435435
436436 @contextmanager
@@ -478,7 +478,7 @@ class log_action(object):
478478 extra = kwargs.setdefault('extra', {})
479479 meta = kwargs.pop('meta', {})
480480 kwpairs = extra.setdefault('kwpairs', {})
481- for k,v in meta.iteritems():
481+ for k, v in meta.iteritems():
482482 kwpairs['meta_%s' % k] = v
483483 extra.update(self._make_extra())
484484 self._logger.log(level, self._action + ': ' + message, *args, **kwargs)
@@ -500,7 +500,7 @@ class log_action(object):
500500
501501 def warning(self, message, *args, **kwargs):
502502 self.log(logging.EXCEPTION, message, *args, **kwargs)
503- warn=warning
503+ warn = warning
504504
505505 def _make_extra(self):
506506 result = dict(self.extra_proto, action=self._action)
@@ -542,7 +542,37 @@ def paging_sanitizer(limit, page, total_count, zero_based_pages=True):
542542 page = min(max(int(page), (0 if zero_based_pages else 1)), max_page)
543543 return limit, page
544544
545-def render_any_markup(name, text, code_mode=False):
545+
546+def _add_inline_line_numbers_to_text(text):
547+ markup_text = '<div class="codehilite"><pre>'
548+ for line_num, line in enumerate(text.splitlines(), 1):
549+ markup_text = markup_text + '<span id="l%s" class="code_block"><span class="lineno">%s</span> %s</span>' % (line_num, line_num, line)
550+ markup_text = markup_text + '</pre></div>'
551+ return markup_text
552+
553+
554+def _add_table_line_numbers_to_text(text):
555+ def _prepend_whitespaces(num, max_num):
556+ num, max_num = str(num), str(max_num)
557+ diff = len(max_num) - len(num)
558+ return ' ' * diff + num
559+
560+ def _len_to_str_column(l, start=1):
561+ max_num = l + start
562+ return '\n'.join(map(_prepend_whitespaces, range(start, max_num), [max_num] * l))
563+
564+ lines = text.splitlines(True)
565+ linenumbers = '<td class="linenos"><div class="linenodiv"><pre>' + _len_to_str_column(len(lines)) + '</pre></div></td>'
566+ markup_text = '<table class="codehilitetable"><tbody><tr>' + linenumbers + '<td class="code"><div class="codehilite"><pre>'
567+ for line_num, line in enumerate(lines, 1):
568+ markup_text = markup_text + '<span id="l%s" class="code_block">%s</span>' % (line_num, line)
569+ markup_text = markup_text + '</pre></div></td></tr></tbody></table>'
570+ return markup_text
571+
572+
573+INLINE = 'inline'
574+TABLE = 'table'
575+def render_any_markup(name, text, code_mode=False, linenumbers_style=TABLE):
546576 """
547577 renders any markup format using the pypeline
548578 Returns jinja-safe text
@@ -552,14 +582,42 @@ def render_any_markup(name, text, code_mode=False):
552582 else:
553583 text = pylons.g.pypeline_markup.render(name, text)
554584 if not pylons.g.pypeline_markup.can_render(name):
555- if code_mode:
556- markup_text = '<div class="codehilite"><pre>'
557- line_num = 1
558- for line in text.splitlines():
559- markup_text = markup_text + '<span id="l%s" class="code_block"><span class="lineno">%s</span> %s</span>' % (line_num, line_num, line)
560- line_num += 1
561- markup_text = markup_text + '</pre></div>'
562- text = markup_text
585+ if code_mode and linenumbers_style == INLINE:
586+ text = _add_inline_line_numbers_to_text(text)
587+ elif code_mode and linenumbers_style == TABLE:
588+ text = _add_table_line_numbers_to_text(text)
563589 else:
564590 text = '<pre>%s</pre>' % text
565591 return Markup(text)
592+
593+# copied from jinja2 dev
594+# latest release, 2.6, implements this incorrectly
595+# can remove and use jinja2 implementation after upgrading to 2.7
596+def do_filesizeformat(value, binary=False):
597+ """Format the value like a 'human-readable' file size (i.e. 13 kB,
598+4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
599+Giga, etc.), if the second parameter is set to `True` the binary
600+prefixes are used (Mebi, Gibi).
601+"""
602+ bytes = float(value)
603+ base = binary and 1024 or 1000
604+ prefixes = [
605+ (binary and 'KiB' or 'kB'),
606+ (binary and 'MiB' or 'MB'),
607+ (binary and 'GiB' or 'GB'),
608+ (binary and 'TiB' or 'TB'),
609+ (binary and 'PiB' or 'PB'),
610+ (binary and 'EiB' or 'EB'),
611+ (binary and 'ZiB' or 'ZB'),
612+ (binary and 'YiB' or 'YB')
613+ ]
614+ if bytes == 1:
615+ return '1 Byte'
616+ elif bytes < base:
617+ return '%d Bytes' % bytes
618+ else:
619+ for i, prefix in enumerate(prefixes):
620+ unit = base ** (i + 2)
621+ if bytes < unit:
622+ return '%.1f %s' % ((base * bytes / unit), prefix)
623+ return '%.1f %s' % ((base * bytes / unit), prefix)
--- a/Allura/allura/lib/patience.py
+++ /dev/null
@@ -1,205 +0,0 @@
1-import sys
2-import difflib
3-from itertools import chain
4-
5-class Region(object):
6- '''Simple utility class that keeps track of subsequences'''
7- __slots__=('seq', # sequence
8- 'l', # lower bound
9- 'h', # upper bound
10- )
11- def __init__(self, seq, l=0, h=None):
12- if h is None: h = len(seq)
13- self.seq, self.l, self.h = seq, l, h
14-
15- def __iter__(self):
16- '''Iterates over the subsequence only'''
17- for i in xrange(self.l, self.h):
18- yield self.seq[i]
19-
20- def __getitem__(self, i):
21- '''works like getitem on the subsequence. Slices return new
22- regions.'''
23- if isinstance(i, slice):
24- start, stop, step = i.indices(len(self))
25- assert step == 1
26- return self.clone(l=self.l+start,h=self.l+stop)
27- elif i >= 0:
28- return self.seq[i+self.l]
29- else:
30- return self.seq[i+self.h]
31-
32- def __len__(self):
33- return self.h - self.l
34-
35- def __repr__(self):
36- if len(self) > 8:
37- srepr = '[%s,...]' % (','.join(repr(x) for x in self[:5]))
38- else:
39- srepr = repr(list(self))
40- return '<Region (%s,%s): %s>' % (self.l, self.h, srepr)
41-
42- def clone(self, **kw):
43- '''Return a new Region based on this one with the
44- provided kw arguments replaced in the constructor.
45- '''
46- kwargs = dict(seq=self.seq, l=self.l, h=self.h)
47- kwargs.update(kw)
48- return Region(**kwargs)
49-
50-def region_diff(ra, rb):
51- '''generator yielding up to two matching blocks, one at the
52- beginning of the region and one at the end. This function
53- mutates the a and b regions, removing any matched blocks.
54- '''
55- # Yield match at the beginning
56- i = 0
57- while i < len(ra) and i < len(rb) and ra[i] == rb[i]:
58- i += 1
59- if i:
60- yield ra.l, rb.l, i
61- ra.l+=i
62- rb.l+=i
63- # Yield match at the end
64- j = 0
65- while j < len(ra) and j < len(rb) and ra[-j-1]==rb[-j-1]:
66- j+=1
67- if j:
68- yield ra.h-j, rb.h-j, j
69- ra.h-=j
70- rb.h-=j
71-
72-def unique(a):
73- '''generator yielding unique lines of a sequence and their positions'''
74- count = {}
75- for aa in a:
76- count[aa] = count.get(aa, 0) + 1
77- for i, aa in enumerate(a):
78- if count[aa] == 1: yield aa, i
79-
80-def common_unique(a, b):
81- '''generator yielding pairs i,j where
82- a[i] == b[j] and a[i] and b[j] are unique within a and b,
83- in increasing j order.'''
84- uq_a = dict(unique(a))
85- for bb, j in unique(b):
86- try:
87- yield uq_a[bb], j
88- except KeyError, ke:
89- continue
90-
91-def patience(seq):
92- stacks = []
93- for i, j in seq:
94- last_top = None
95- for stack in stacks:
96- top_i, top_j, top_back = stack[-1]
97- if top_i > i:
98- stack.append((i, j, last_top))
99- break
100- last_top = len(stack)-1
101- else:
102- stacks.append([(i, j, last_top)])
103- if not stacks: return []
104- prev = len(stacks[-1])-1
105- seq = []
106- for stack in reversed(stacks):
107- top_i, top_j, top_back = stack[prev]
108- seq.append((top_i, top_j))
109- prev = top_back
110- return reversed(seq)
111-
112-def match_core(a, b):
113- '''Returns blocks that match between sequences a and b as
114- (index_a, index_b, size)
115- '''
116- ra = Region(a)
117- rb = Region(b)
118- # beginning/end match
119- for block in region_diff(ra,rb): yield block
120- # patience core
121- last_i = last_j = None
122- for i, j in chain(
123- patience(common_unique(ra, rb)),
124- [(ra.h, rb.h)]):
125- if last_i is not None:
126- for block in region_diff(ra[last_i:i], rb[last_j:j]):
127- yield block
128- last_i = i
129- last_j = j
130-
131-def diff_gen(a, b, opcode_gen):
132- '''Convert a sequence of SequenceMatcher opcodes to
133- unified diff-like output
134- '''
135- def _iter():
136- for op, i1, i2, j1, j2 in opcode_gen:
137- if op == 'equal':
138- yield ' ', Region(a, i1, i2)
139- if op in ('delete', 'replace'):
140- yield '- ', Region(a, i1, i2)
141- if op in ('replace', 'insert'):
142- yield '+ ', Region(b, j1, j2)
143- for prefix, rgn in _iter():
144- for line in rgn:
145- yield prefix, line
146-
147-def unified_diff(
148- a, b, fromfile='', tofile='', fromfiledate='',
149- tofiledate='', n=3, lineterm='\n'):
150- started = False
151- for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
152- if not started:
153- yield '--- %s %s%s' % (fromfile, fromfiledate, lineterm)
154- yield '+++ %s %s%s' % (tofile, tofiledate, lineterm)
155- started = True
156- i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
157- yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
158- for tag, i1, i2, j1, j2 in group:
159- if tag == 'equal':
160- for line in a[i1:i2]:
161- yield ' ' + line
162- continue
163- if tag == 'replace' or tag == 'delete':
164- for line in a[i1:i2]:
165- yield '-' + line
166- if tag == 'replace' or tag == 'insert':
167- for line in b[j1:j2]:
168- yield '+' + line
169-
170-class SequenceMatcher(difflib.SequenceMatcher):
171-
172- def get_matching_blocks(self):
173- '''Uses patience diff algorithm to find matching blocks.'''
174- if self.matching_blocks is not None:
175- return self.matching_blocks
176- matching_blocks = list(match_core(self.a, self.b))
177- matching_blocks.append((len(self.a), len(self.b), 0))
178- self.matching_blocks = sorted(matching_blocks)
179- return self.matching_blocks
180-
181-def test(): # pragma no cover
182- if len(sys.argv) == 3:
183- fn_a = sys.argv[1]
184- fn_b = sys.argv[2]
185- else:
186- fn_a = 'a.c'
187- fn_b = 'b.c'
188- a = open(fn_a).readlines()
189- b = open(fn_b).readlines()
190- # print '====', fn_a
191- # sys.stdout.write(''.join(a))
192- # print '====', fn_b
193- # sys.stdout.write(''.join(b))
194- sm = SequenceMatcher(None, a, b)
195- # print 'Patience opcodes:', sm.get_opcodes()
196- print ''.join(unified_diff(a, b)) #pragma:printok
197- # for prefix, line in diff_gen(a, b, sm.get_opcodes()):
198- # sys.stdout.write(''.join((prefix, line)))
199- # sm = difflib.SequenceMatcher(None, a, b)
200- # print 'Difflib opcodes:', sm.get_opcodes()
201- # for prefix, line in diff_gen(a, b, sm.get_opcodes()):
202- # sys.stdout.write(''.join((prefix, line)))
203-
204-if __name__ == '__main__': # pragma no cover
205- test()
--- a/Allura/allura/model/notification.py
+++ b/Allura/allura/model/notification.py
@@ -121,7 +121,7 @@ class Notification(MappedClass):
121121 file_info.file.seek(0, 2)
122122 bytecount = file_info.file.tell()
123123 file_info.file.seek(0)
124- text = "%s\n%s (%s bytes in %s)" % (text, file_info.filename, bytecount, file_info.type)
124+ text = "%s\n\n\nAttachment: %s (%s; %s)" % (text, file_info.filename, h.do_filesizeformat(bytecount), file_info.type)
125125
126126 subject = post.subject or ''
127127 if post.parent_id and not subject.lower().startswith('re:'):
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -123,6 +123,7 @@ class Project(MappedClass):
123123 shortname = FieldProperty(str)
124124 name=FieldProperty(str)
125125 notifications_disabled = FieldProperty(bool)
126+ suppress_emails = FieldProperty(bool)
126127 show_download_button=FieldProperty(S.Deprecated)
127128 short_description=FieldProperty(str, if_missing='')
128129 summary=FieldProperty(str, if_missing='')
@@ -388,11 +389,17 @@ class Project(MappedClass):
388389 delta_ordinal = 0
389390 max_ordinal = 0
390391 neighborhood_admin_mode = False
392+
393+ if self.is_user_project:
394+ entries.append({'ordinal': delta_ordinal, 'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")})
395+ max_ordinal = delta_ordinal
396+ delta_ordinal = delta_ordinal + 1
397+
391398 if self == self.neighborhood.neighborhood_project:
392- delta_ordinal = 1
399+ entries.append({'ordinal':delta_ordinal, 'entry':SitemapEntry('Home', self.neighborhood.url(), ui_icon="tool-home")})
400+ max_ordinal = delta_ordinal
401+ delta_ordinal = delta_ordinal + 1
393402 neighborhood_admin_mode = True
394- entries.append({'ordinal':0,'entry':SitemapEntry('Home', self.neighborhood.url(), ui_icon="tool-home")})
395-
396403
397404 for sub in self.direct_subprojects:
398405 ordinal = sub.ordinal + delta_ordinal
@@ -417,9 +424,6 @@ class Project(MappedClass):
417424 entries.append({'ordinal': max_ordinal + 1,'entry':SitemapEntry('Moderate', "%s_moderate/" % self.neighborhood.url(), ui_icon="tool-admin")})
418425 max_ordinal += 1
419426
420- if self.is_user_project:
421- entries.append({'ordinal': max_ordinal + 1,'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")})
422-
423427 entries = sorted(entries, key=lambda e: e['ordinal'])
424428 for e in entries:
425429 sitemap.children.append(e['entry'])
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -5,6 +5,7 @@ import mimetypes
55 import logging
66 import string
77 import re
8+from difflib import SequenceMatcher
89 from hashlib import sha1
910 from datetime import datetime
1011 from collections import defaultdict
@@ -20,7 +21,6 @@ from ming.utils import LazyProperty
2021 from ming.orm import FieldProperty, session, Mapper
2122 from ming.orm.declarative import MappedClass
2223
23-from allura.lib.patience import SequenceMatcher
2424 from allura.lib import helpers as h
2525 from allura.lib import utils
2626
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -20,7 +20,7 @@ SEE: https://groups.google.com/group/compass-users/browse_thread/thread/df09f674
2020 * Color variables for the theme.
2121 *
2222 */
23-/*
23+/*
2424 * Your run-of-the-mill reset CSS, inspired by:
2525 *
2626 * yui.yahooapis.com/2.8.1/build/base/base.css
@@ -99,7 +99,7 @@ input, select {
9999 background: #fff;
100100 }
101101
102-/*
102+/*
103103 * Setup a minimal, baseline CSS, layered on top of a reset
104104 * to define the default styles we've come to expect. Inspired by:
105105 *
@@ -245,7 +245,7 @@ h2.title, #site-header nav {
245245 font-weight: normal;
246246 }
247247
248-/*
248+/*
249249 * General CSS rules governing high-level elements.
250250 *
251251 */
@@ -380,7 +380,7 @@ blockquote {
380380 padding-left: 1em;
381381 }
382382
383-/*
383+/*
384384 * Style elements in the main header and footer areas.
385385 *
386386 */
@@ -2707,3 +2707,43 @@ h1.title .viewer:hover {
27072707 .neighborhood_feed_entry h3 {
27082708 font-size: 1.1em;
27092709 }
2710+
2711+/*linenumbers in codeblock viewer style*/
2712+
2713+table.codehilitetable {
2714+ background: #F8F8F8;
2715+ margin-left:0px;
2716+}
2717+
2718+td.linenos {
2719+ width:auto;
2720+ padding: 0;
2721+}
2722+div.linenodiv {
2723+
2724+}
2725+td.linenos pre {
2726+ font-size: 100%;
2727+ padding: 1px;
2728+ padding-left: 7px;
2729+ padding-right: 5px;
2730+ margin-left: 15px;
2731+ background-color: #EBEBEB;
2732+ color: #555;
2733+ border-right: solid 1px #DDD;
2734+}
2735+td.code {
2736+ padding-left: 0px;
2737+ width:100%;
2738+}
2739+
2740+div.codehilite pre {
2741+ padding-left: 0px;
2742+ padding-top:10px;
2743+ padding-bottom:10px;
2744+}
2745+div.codehilite pre div.code_block {
2746+ padding-left:10px;
2747+ width: 97%;
2748+}
2749+
--- a/Allura/allura/public/nf/css/forge/hilite.css
+++ b/Allura/allura/public/nf/css/forge/hilite.css
@@ -66,4 +66,9 @@
6666 .codehilite div { margin:0; padding: 0; }
6767 .codehilite .code_block { width:100%; }
6868 .codehilite .code_block:hover { background-color: #ffff99; }
69-.codehilite .lineno { background-color: #ebebeb; display:inline-block; padding:0 .5em; border-width: 0 1px 0 0; border-style: solid; border-color: #ddd; }
69+.codehilite .lineno { background-color: #ebebeb;
70+ display:inline-block;
71+ padding:0 .5em;
72+ border-width: 0 1px 0 0;
73+ border-style: solid;
74+ border-color: #ddd; }
--- a/Allura/allura/tasks/repo_tasks.py
+++ b/Allura/allura/tasks/repo_tasks.py
@@ -1,10 +1,13 @@
11 import shutil
22 import logging
3+import traceback
34
45 from pylons import c
56
67 from allura.lib.decorators import task
78 from allura.lib.repository import RepositoryApp
9+from allura.lib import helpers as h
10+from allura.tasks.mail_tasks import sendmail
811
912 @task
1013 def init(**kwargs):
@@ -20,15 +23,49 @@ def clone(
2023 cloned_from_path,
2124 cloned_from_name,
2225 cloned_from_url):
23- from allura import model as M
24- c.app.repo.init_as_clone(
25- cloned_from_path,
26- cloned_from_name,
27- cloned_from_url)
28- M.Notification.post_user(
29- c.user, c.app.repo, 'created',
30- text='Repository %s/%s created' % (
31- c.project.shortname, c.app.config.options.mount_point))
26+ try:
27+ from allura import model as M
28+ c.app.repo.init_as_clone(
29+ cloned_from_path,
30+ cloned_from_name,
31+ cloned_from_url)
32+ M.Notification.post_user(
33+ c.user, c.app.repo, 'created',
34+ text='Repository %s/%s created' % (
35+ c.project.shortname, c.app.config.options.mount_point))
36+ if not c.project.suppress_emails:
37+ sendmail(
38+ destinations=[str(c.user._id)],
39+ fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>',
40+ reply_to=u'noreply@in.sf.net',
41+ subject=u'SourceForge Repo Clone Complete',
42+ message_id=h.gen_message_id(),
43+ text=u''.join([
44+ u'Clone of repo %s in project %s from %s is complete. Your repo is now ready to use.\n'
45+ ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url))
46+ except:
47+ sendmail(
48+ destinations=['sfengineers@geek.net'],
49+ fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>',
50+ reply_to=u'noreply@in.sf.net',
51+ subject=u'SourceForge Repo Clone Failure',
52+ message_id=h.gen_message_id(),
53+ text=u''.join([
54+ u'Clone of repo %s in project %s from %s failed.\n',
55+ u'\n',
56+ u'%s',
57+ ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url, traceback.format_exc()))
58+ if not c.project.suppress_emails:
59+ sendmail(
60+ destinations=[str(c.user._id)],
61+ fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>',
62+ reply_to=u'noreply@in.sf.net',
63+ subject=u'SourceForge Repo Clone Failed',
64+ message_id=h.gen_message_id(),
65+ text=u''.join([
66+ u'Clone of repo %s in project %s from %s failed. ',
67+ u'The SourceForge engineering team has been notified.\n',
68+ ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url))
3269
3370 @task
3471 def reclone(*args, **kwargs):
--- a/Allura/allura/templates/repo/file.html
+++ b/Allura/allura/templates/repo/file.html
@@ -70,7 +70,7 @@
7070 <div class="clip grid-19">
7171 <h3><span class="ico-l"><b data-icon="{{g.icons['table'].char}}" class="ico {{g.icons['table'].css}}"></b> {{h.really_unicode(blob.name)}}</span></h3>
7272 {% if blob.has_pypeline_view %}
73- {{h.render_any_markup(blob.name, blob.text, code_mode=True)}}
73+ {{h.render_any_markup(blob.name, blob.text, code_mode=True, linenumbers_style=h.TABLE)}}
7474 {% else %}
7575 {{g.highlight(blob.text, filename=blob.name)}}
7676 {% endif %}
--- a/Allura/allura/templates/repo/fork.html
+++ b/Allura/allura/templates/repo/fork.html
@@ -16,9 +16,13 @@
1616 {% endfor %}
1717 </select>
1818 </div>
19- <label class="grid-4">Repository Name:</label>
19+ <label class="grid-4">Label:</label>
2020 <div class="grid-15">
21- <input type="text" name="to_name" value="{{to_name}}"/>
21+ <input type="text" name="mount_label" value="{{mount_label}}"/>
22+ </div>
23+ <label class="grid-4">Mount point:</label>
24+ <div class="grid-15">
25+ <input type="text" name="mount_point" value="{{mount_point}}"/>
2226 </div>
2327 <label class="grid-4">&nbsp;</label>
2428 <div class="grid-15">
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -8,6 +8,7 @@ class TestUserProfile(TestController):
88 @td.with_user_project('test-admin')
99 def test_profile(self):
1010 response = self.app.get('/u/test-admin/profile/')
11+ assert '<h2 class="dark title">Test Admin' in response
1112 assert 'OpenIDs' in response
1213 response = self.app.get('/u/test-admin/profile/configuration')
1314 assert 'Configure Dashboard' in response
--- a/Allura/allura/tests/model/test_discussion.py
+++ b/Allura/allura/tests/model/test_discussion.py
@@ -167,7 +167,7 @@ def test_attachment_methods():
167167 p = t.post(text=u'test message', forum= None, subject= '', file_info=fs)
168168 ThreadLocalORMSession.flush_all()
169169 n = M.Notification.query.get(subject=u'[test:wiki] Test comment notification')
170- assert u'test message\nfake.txt (37 bytes in text/plain)'==n.text
170+ assert_equals(u'test message\n\n\nAttachment: fake.txt (37 Bytes; text/plain)', n.text)
171171
172172 @with_setup(setUp, tearDown)
173173 def test_discussion_delete():
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -1,11 +1,13 @@
11 from nose.tools import assert_raises
22
3+from ming.orm.ormsession import ThreadLocalORMSession
4+
35 from alluratest.controller import setup_basic_test, setup_global_objects
4-from allura.command import script, set_neighborhood_features
6+from allura.command import script, set_neighborhood_features, rssfeeds
57 from allura import model as M
8+from forgeblog import model as BM
69 from allura.lib.exceptions import InvalidNBFeatureValueError
710
8-
911 test_config = 'test.ini#main'
1012
1113 class EmptyClass(object): pass
@@ -116,3 +118,21 @@ def test_set_neighborhood_css():
116118 assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', '2.8'])
117119 assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'None'])
118120 assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'True'])
121+
122+def test_pull_rss_feeds():
123+ base_app = M.AppConfig.query.find().all()[0]
124+ tmp_app = M.AppConfig(tool_name=u'Blog', discussion_id=base_app.discussion_id,
125+ project_id=base_app.project_id,
126+ options={u'ordinal': 0, u'show_right_bar': True,
127+ u'project_name': base_app.project.name,
128+ u'mount_point': u'blog',
129+ u'mount_label': u'Blog'})
130+ new_external_feeds = ['http://wordpress.org/news/feed/']
131+ BM.Globals(app_config_id=tmp_app._id, external_feeds=new_external_feeds)
132+ ThreadLocalORMSession.flush_all()
133+
134+ cmd = rssfeeds.RssFeedsCommand('pull-rss-feeds')
135+ cmd.run([test_config, '-a', tmp_app._id])
136+ cmd.command()
137+
138+ assert len(BM.BlogPost.query.find({'app_config_id': tmp_app._id}).all()) > 0
--- a/Allura/allura/tests/test_patience.py
+++ /dev/null
@@ -1,76 +0,0 @@
1-from os import path, environ
2-from collections import defaultdict
3-
4-from allura.lib import patience
5-
6-def text2lines(text):
7- return [l + '\n' for l in text.split('\n')]
8-
9-def test_region():
10- r = patience.Region('foobar')
11- r2 = r.clone()
12- assert id(r) != id(r2)
13- assert '-'.join(r) == '-'.join(r2)
14- subr = r[1:5]
15- assert type(subr) is type(r)
16- assert ''.join(subr) == ''.join(r)[1:5]
17- repr(r)
18- repr(patience.Region('fffffffffffffffffffffffffffffffffffffffff'))
19-
20-def test_unified_diff():
21- text1 = '''\
22-from paste.deploy import loadapp
23-from paste.deploy import loadapp
24-from paste.deploy import loadapp
25-from paste.deploy import loadapp
26-from paste.script.appinstall import SetupCommand
27-from paste.script.appinstall import SetupCommand
28-from paste.script.appinstall import SetupCommand
29-from paste.script.appinstall import SetupCommand
30-from paste.deploy import appconfig
31-'''
32- text2 = '''\
33-from paste.deploy import loadapp
34-from paste.deploy import loadapp
35-from paste.deploy import loadapp
36-from paste.deploy import loadapp
37-from paste.script.appinstall import SetupCommand2
38-from paste.script.appinstall import SetupCommand3
39-from paste.script.appinstall import SetupCommand4
40-from paste.deploy import appconfig
41-'''
42- line_uni_diff = '''\
43- from paste.deploy import loadapp
44- from paste.deploy import loadapp
45- from paste.deploy import loadapp
46--from paste.script.appinstall import SetupCommand
47--from paste.script.appinstall import SetupCommand
48--from paste.script.appinstall import SetupCommand
49--from paste.script.appinstall import SetupCommand
50-+from paste.script.appinstall import SetupCommand2
51-+from paste.script.appinstall import SetupCommand3
52-+from paste.script.appinstall import SetupCommand4
53- from paste.deploy import appconfig'''
54-
55- line_diff = '''\
56- from paste.deploy import loadapp
57-''' + line_uni_diff
58-
59- lines1 = text2lines(text1)
60- lines2 = text2lines(text2)
61- diff = patience.unified_diff(lines1, lines2)
62- diff = ''.join(diff)
63- assert diff == '''\
64----
65-+++
66-@@ -2,9 +2,8 @@
67-%s
68-
69-''' % line_uni_diff, '=' + diff + '='
70-
71- sm = patience.SequenceMatcher(None, lines1, lines2)
72- buf = ''
73- for prefix, line in patience.diff_gen(lines1, lines2, sm.get_opcodes()):
74- assert prefix[1] == ' '
75- buf += prefix[0] + line
76- assert buf == line_diff + '\n \n', '=' + buf + '='
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -113,6 +113,7 @@ setup(
113113 create-neighborhood = allura.command:CreateNeighborhoodCommand
114114 create-trove-categories = allura.command:CreateTroveCategoriesCommand
115115 set-neighborhood-features = allura.command:SetNeighborhoodFeaturesCommand
116+ pull-rss-feeds = allura.command.rssfeeds:RssFeedsCommand
116117
117118 [easy_widgets.resources]
118119 ew_resources=allura.config.resources:register_ew_resources
--- a/Allura/test.ini
+++ b/Allura/test.ini
@@ -75,7 +75,7 @@ scm.clone.svn = svn checkout --username=$username $source_url $dest_path
7575
7676 scm.repos.root = /tmp
7777
78-stats.sample_rate=0
78+#stats.sample_rate = 0
7979
8080 disable_csrf_protection=1
8181
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -12,12 +12,14 @@ from pylons import g, c, request, response
1212 from formencode import validators
1313 from webob import exc
1414
15+from ming.orm import session
16+
1517 # Pyforge-specific imports
1618 from allura.app import Application, ConfigOption, SitemapEntry
1719 from allura.app import DefaultAdminController
1820 from allura.lib import helpers as h
1921 from allura.lib.search import search
20-from allura.lib.decorators import require_post
22+from allura.lib.decorators import require_post, Property
2123 from allura.lib.security import has_access, require_access
2224 from allura.lib import widgets as w
2325 from allura.lib.widgets.subscriptions import SubscribeForm
@@ -56,6 +58,7 @@ class ForgeBlogApp(Application):
5658 ordinal=14
5759 installable=True
5860 config_options = Application.config_options
61+ default_external_feeds = []
5962 icons={
6063 24:'images/blog_24.png',
6164 32:'images/blog_32.png',
@@ -67,6 +70,24 @@ class ForgeBlogApp(Application):
6770 self.root = RootController()
6871 self.admin = BlogAdminController(self)
6972
73+ @Property
74+ def external_feeds_list():
75+ def fget(self):
76+ globals = BM.Globals.query.get(app_config_id=self.config._id)
77+ if globals is not None:
78+ external_feeds = globals.external_feeds
79+ else:
80+ external_feeds = self.default_external_feeds
81+ return external_feeds
82+ def fset(self, new_external_feeds):
83+ globals = BM.Globals.query.get(app_config_id=self.config._id)
84+ if globals is not None:
85+ globals.external_feeds = new_external_feeds
86+ elif len(new_external_feeds) > 0:
87+ globals = BM.Globals(app_config_id=self.config._id, external_feeds=new_external_feeds)
88+ if globals is not None:
89+ session(globals).flush()
90+
7091 @property
7192 @h.exceptionless([], log)
7293 def sitemap(self):
@@ -94,7 +115,12 @@ class ForgeBlogApp(Application):
94115 return links
95116
96117 def admin_menu(self):
97- return super(ForgeBlogApp, self).admin_menu(force_options=True)
118+ admin_url = c.project.url() + 'admin/' + self.config.options.mount_point + '/'
119+ # temporarily disabled until some bugs are fixed
120+ links = []#[SitemapEntry('External feeds', admin_url + 'exfeed', className='admin_modal')]
121+ links += super(ForgeBlogApp, self).admin_menu(force_options=True)
122+ return links
123+ #return super(ForgeBlogApp, self).admin_menu(force_options=True)
98124
99125 def install(self, project):
100126 'Set up any default permissions and roles here'
@@ -170,7 +196,7 @@ class RootController(BaseController):
170196 require_access(c.app, 'write')
171197 now = datetime.utcnow()
172198 post = dict(
173- state='draft')
199+ state='published')
174200 c.form = W.new_post_form
175201 return dict(post=post)
176202
@@ -359,3 +385,34 @@ class BlogAdminController(DefaultAdminController):
359385 self.app.config.options['show_discussion'] = show_discussion and True or False
360386 flash('Blog options updated')
361387 redirect(h.really_unicode(c.project.url()+'admin/tools').encode('utf-8'))
388+
389+ @without_trailing_slash
390+ @expose('jinja:forgeblog:templates/blog/admin_exfeed.html')
391+ def exfeed(self):
392+ #self.app.external_feeds_list = ['feed1', 'feed2']
393+ #log.info("EXFEED: %s" % self.app.external_feeds_list)
394+ feeds_list = []
395+ for feed in self.app.external_feeds_list:
396+ feeds_list.append(feed)
397+ return dict(app=self.app,
398+ feeds_list=feeds_list,
399+ allow_config=has_access(self.app, 'configure')())
400+
401+ @without_trailing_slash
402+ @expose()
403+ @require_post()
404+ def set_exfeed(self, **kw):
405+ new_exfeed = kw.get('new_exfeed', None)
406+ exfeed_val = kw.get('exfeed', [])
407+ if type(exfeed_val) == unicode:
408+ exfeed_list = []
409+ exfeed_list.append(exfeed_val)
410+ else:
411+ exfeed_list = exfeed_val
412+
413+ if new_exfeed is not None and new_exfeed != '':
414+ exfeed_list.append(new_exfeed)
415+
416+ self.app.external_feeds_list = exfeed_list
417+ flash('External feeds updated')
418+ redirect(c.project.url()+'admin/tools')
--- a/ForgeBlog/forgeblog/model/__init__.py
+++ b/ForgeBlog/forgeblog/model/__init__.py
@@ -1 +1 @@
1-from blog import BlogPost, Attachment, BlogPostSnapshot
1+from blog import Globals, BlogPost, Attachment, BlogPostSnapshot
--- a/ForgeBlog/forgeblog/model/blog.py
+++ b/ForgeBlog/forgeblog/model/blog.py
@@ -1,3 +1,4 @@
1+import difflib
12 from datetime import datetime
23 from random import randint
34
@@ -9,13 +10,28 @@ from pymongo.errors import DuplicateKeyError
910
1011 from ming import schema
1112 from ming.orm import FieldProperty, ForeignIdProperty, Mapper, session, state
13+from ming.orm.declarative import MappedClass
14+
1215 from allura import model as M
1316 from allura.lib import helpers as h
14-from allura.lib import utils, patience
17+from allura.lib import utils
1518
1619 config = utils.ConfigProxy(
1720 common_suffix='forgemail.domain')
1821
22+class Globals(MappedClass):
23+
24+ class __mongometa__:
25+ name = 'blog-globals'
26+ session = M.project_orm_session
27+ indexes = [ 'app_config_id' ]
28+
29+ type_s = 'BlogGlobals'
30+ _id = FieldProperty(schema.ObjectId)
31+ app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
32+ external_feeds=FieldProperty([str])
33+
34+
1935 class BlogPostSnapshot(M.Snapshot):
2036 class __mongometa__:
2137 name='blog_post_snapshot'
@@ -166,7 +182,7 @@ class BlogPost(M.VersionedArtifact):
166182 v2 = self
167183 la = [ line + '\n' for line in v1.text.splitlines() ]
168184 lb = [ line + '\n' for line in v2.text.splitlines() ]
169- diff = ''.join(patience.unified_diff(
185+ diff = ''.join(difflib.unified_diff(
170186 la, lb,
171187 'v%d' % v1.version,
172188 'v%d' % v2.version))
--- /dev/null
+++ b/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html
@@ -0,0 +1,27 @@
1+<form method="POST" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/set_exfeed">
2+ <label class="grid-13">Existing external feeds:</label>
3+ <div class="grid-13">
4+ <ul>
5+ {% if allow_config %}
6+ {% for feed in feeds_list %}
7+ <li><input type="checkbox" name="exfeed" value="{{ feed }}" checked="checked"><span>{{ feed }}</span></li>
8+ {% endfor %}
9+ {% else %}
10+ {% for feed in feeds_list %}
11+ <li><span>{{ feed.value }}</span></li>
12+ {% endfor %}
13+ {% endif %}
14+ </div>
15+ {% if allow_config %}
16+ <div class="grid-13">&nbsp;</div>
17+ <div class="grid-13">
18+ <input type="text" name="new_exfeed" id="new_exfeed" value=""/>
19+ </div>
20+ <div class="grid-13">&nbsp;</div>
21+ <hr>
22+ <div class="grid-13">&nbsp;</div>
23+ <div class="grid-13">
24+ <input type="submit" value="Save"/>
25+ </div>
26+ {% endif %}
27+</form>
--- a/ForgeBlog/forgeblog/tests/functional/test_root.py
+++ b/ForgeBlog/forgeblog/tests/functional/test_root.py
@@ -56,6 +56,7 @@ class TestRootController(TestController):
5656
5757 def test_root_new_post(self):
5858 response = self.app.get('/blog/new')
59+ assert '<option selected value="published">Published</option>' in response
5960 assert 'Enter your title here' in response
6061
6162 def test_validation(self):
--- a/ForgeGit/forgegit/tests/functional/test_controllers.py
+++ b/ForgeGit/forgegit/tests/functional/test_controllers.py
@@ -37,18 +37,26 @@ class TestRootController(TestController):
3737 ThreadLocalORMSession.close_all()
3838
3939 def test_fork(self):
40+ r = self.app.get('%sfork/' % c.app.repo.url())
41+ assert '<input type="text" name="mount_point" value="test"/>' in r
42+ assert '<input type="text" name="mount_label" value="test - Git"/>' in r
43+
4044 to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
45+ mount_point = 'reponame'
4146 r = self.app.post('/src-git/fork', params=dict(
4247 project_id=str(to_project._id),
43- to_name='code'))
48+ mount_point=mount_point,
49+ mount_label='Test forked repository'))
50+ assert "{status: 'error'}" not in str(r.follow())
4451 cloned_from = c.app.repo
45- with h.push_context('test2', 'code', neighborhood='Projects'):
52+ with h.push_context('test2', mount_point, neighborhood='Projects'):
4653 c.app.repo.init_as_clone(
4754 cloned_from.full_fs_path,
4855 cloned_from.app.config.script_name(),
4956 cloned_from.full_fs_path)
50- r = self.app.get('/p/test2/code').follow().follow().follow()
57+ r = self.app.get('/p/test2/%s' % mount_point).follow().follow().follow()
5158 assert 'Clone of' in r
59+ assert 'Test forked repository' in r
5260 r = self.app.get('/src-git/').follow().follow()
5361 assert 'Forks' in r
5462
@@ -56,7 +64,7 @@ class TestRootController(TestController):
5664 to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
5765 r = self.app.post('/src-git/fork', params=dict(
5866 project_id=str(to_project._id),
59- to_name='code'))
67+ mount_point='code'))
6068 cloned_from = c.app.repo
6169 with h.push_context('test2', 'code', neighborhood='Projects'):
6270 c.app.repo.init_as_clone(
@@ -147,8 +155,8 @@ class TestRootController(TestController):
147155 def test_file(self):
148156 ci = self._get_ci()
149157 resp = self.app.get(ci + 'tree/README')
150- assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2]
151- content = str(resp.html.find('div',{'class':'clip grid-19'}))
158+ assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2]
159+ content = str(resp.html.find('div', {'class':'clip grid-19'}))
152160 assert 'This is readme' in content, content
153161 assert '<span id="l1" class="code_block">' in resp
154162 assert 'var hash = window.location.hash.substring(1);' in resp
@@ -174,6 +182,6 @@ class TestRootController(TestController):
174182 def test_file_force_display(self):
175183 ci = self._get_ci()
176184 resp = self.app.get(ci + 'tree/README?force=True')
177- content = str(resp.html.find('div',{'class':'clip grid-19'}))
185+ content = str(resp.html.find('div', {'class':'clip grid-19'}))
178186 assert re.search(r'<pre>.*This is readme', content), content
179187 assert '</pre>' in content, content
--- a/ForgeHg/forgehg/tests/functional/test_controllers.py
+++ b/ForgeHg/forgehg/tests/functional/test_controllers.py
@@ -1,6 +1,9 @@
11 import json
22
33 import pkg_resources
4+import pylons
5+pylons.c = pylons.tmpl_context
6+pylons.g = pylons.app_globals
47 from pylons import c
58 from ming.orm import ThreadLocalORMSession
69 from datadiff.tools import assert_equal
@@ -35,7 +38,8 @@ class TestRootController(TestController):
3538 to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
3639 r = self.app.post('/src-hg/fork', params=dict(
3740 project_id=str(to_project._id),
38- to_name='code'))
41+ mount_point='code'))
42+ assert "{status: 'error'}" not in str(r.follow())
3943 cloned_from = c.app.repo
4044 with h.push_context('test2', 'code', neighborhood='Projects'):
4145 c.app.repo.init_as_clone(
@@ -51,7 +55,8 @@ class TestRootController(TestController):
5155 to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
5256 r = self.app.post('/src-hg/fork', params=dict(
5357 project_id=str(to_project._id),
54- to_name='code'))
58+ mount_point='code'))
59+ assert "{status: 'error'}" not in str(r.follow())
5560 cloned_from = c.app.repo
5661 with h.push_context('test2', 'code', neighborhood='Projects'):
5762 c.app.repo.init_as_clone(
@@ -117,14 +122,14 @@ class TestRootController(TestController):
117122 def test_tree(self):
118123 ci = self._get_ci()
119124 resp = self.app.get(ci + 'tree/')
120- assert len(resp.html.findAll('tr')) ==2, resp.showbrowser()
125+ assert len(resp.html.findAll('tr')) == 2, resp.showbrowser()
121126 assert 'README' in resp, resp.showbrowser()
122127
123128 def test_file(self):
124129 ci = self._get_ci()
125130 resp = self.app.get(ci + 'tree/README')
126- assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2]
127- content = str(resp.html.find('div',{'class':'clip grid-19'}))
131+ assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2]
132+ content = str(resp.html.find('div', {'class':'clip grid-19'}))
128133 assert 'This is readme' in content, content
129134 assert '<span id="l1" class="code_block">' in resp
130135 assert 'var hash = window.location.hash.substring(1);' in resp
--- a/ForgeSVN/forgesvn/model/svn.py
+++ b/ForgeSVN/forgesvn/model/svn.py
@@ -259,12 +259,15 @@ class SVNImplementation(M.RepositoryImplementation):
259259 log.info('ClientError processing %r %r, treating as empty', ci, self._repo, exc_info=True)
260260 log_entry = Object(date=0, message='', changed_paths=[])
261261 # Save commit metadata
262+ log_date = None
263+ if hasattr(log_entry, 'date'):
264+ log_date = datetime.utcfromtimestamp(log_entry.date)
262265 ci.committed = Object(
263266 name=log_entry.get('author', '--none--'),
264267 email='',
265- date=datetime.utcfromtimestamp(log_entry.date))
268+ date=log_date)
266269 ci.authored=Object(ci.committed)
267- ci.message=log_entry.message
270+ ci.message=log_entry.get("message", "--none--")
268271 if revno > 1:
269272 parent_oid = self._oid(revno - 1)
270273 ci.parent_ids = [ parent_oid ]
@@ -278,13 +281,14 @@ class SVNImplementation(M.RepositoryImplementation):
278281 D=ci.diffs.removed,
279282 M=ci.diffs.changed,
280283 R=ci.diffs.changed)
281- for path in log_entry.changed_paths:
282- if path.copyfrom_path:
283- ci.diffs.copied.append(dict(
284- old=h.really_unicode(path.copyfrom_path),
285- new=h.really_unicode(path.path)))
286- continue
287- lst[path.action].append(h.really_unicode(path.path))
284+ if hasattr(log_entry, 'changed_paths'):
285+ for path in log_entry.changed_paths:
286+ if path.copyfrom_path:
287+ ci.diffs.copied.append(dict(
288+ old=h.really_unicode(path.copyfrom_path),
289+ new=h.really_unicode(path.path)))
290+ continue
291+ lst[path.action].append(h.really_unicode(path.path))
288292
289293 def refresh_commit_info(self, oid, seen_object_ids, lazy=True):
290294 from allura.model.repo import CommitDoc
@@ -301,15 +305,18 @@ class SVNImplementation(M.RepositoryImplementation):
301305 except pysvn.ClientError:
302306 log.info('ClientError processing %r %r, treating as empty', oid, self._repo, exc_info=True)
303307 log_entry = Object(date='', message='', changed_paths=[])
308+ log_date = None
309+ if hasattr(log_entry, 'date'):
310+ log_date = datetime.utcfromtimestamp(log_entry.date)
304311 user = Object(
305312 name=log_entry.get('author', '--none--'),
306313 email='',
307- date=datetime.utcfromtimestamp(log_entry.date))
314+ date=log_date)
308315 args = dict(
309316 tree_id=None,
310317 committed=user,
311318 authored=user,
312- message=log_entry.message,
319+ message=log_entry.get("message", "--none--"),
313320 parent_ids=[],
314321 child_ids=[])
315322 if revno > 1:
--- a/ForgeSVN/forgesvn/tests/functional/test_controllers.py
+++ b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
@@ -73,8 +73,8 @@ class TestRootController(TestController):
7373
7474 def test_file(self):
7575 resp = self.app.get('/src/1/tree/README')
76- assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2]
77- content = str(resp.html.find('div',{'class':'clip grid-19'}))
76+ assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2]
77+ content = str(resp.html.find('div', {'class':'clip grid-19'}))
7878 assert 'This is readme' in content, content
7979 assert '<span id="l1" class="code_block">' in resp
8080 assert 'var hash = window.location.hash.substring(1);' in resp
--- a/ForgeTracker/forgetracker/data/ticket_changed_tmpl
+++ b/ForgeTracker/forgetracker/data/ticket_changed_tmpl
@@ -1,4 +1,4 @@
1-{% python from allura.lib import patience %}\
1+{% python import difflib %}\
22 {% python from allura.model import User %}\
33 {% for key, values in changelist %}\
44 {% choose %}\
@@ -9,7 +9,7 @@ Diff:
99
1010 ~~~~
1111
12-${'\n'.join(patience.unified_diff(
12+${'\n'.join(difflib.unified_diff(
1313 a=values[0].splitlines(),
1414 b=values[1].splitlines(),
1515 fromfile='old',
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -1,6 +1,7 @@
11 import logging
22 import urllib
33 import json
4+import difflib
45 from datetime import datetime, timedelta
56
67 import bson
@@ -20,7 +21,6 @@ from ming.orm.declarative import MappedClass
2021 from allura.model import Artifact, VersionedArtifact, Snapshot, project_orm_session, BaseAttachment
2122 from allura.model import User, Feed, Thread, Notification, ProjectRole
2223 from allura.model import ACE, ALL_PERMISSIONS, DENY_ALL
23-from allura.lib import patience
2424 from allura.lib import security
2525 from allura.lib.search import search_artifact
2626 from allura.lib import utils
@@ -392,7 +392,7 @@ class Ticket(VersionedArtifact):
392392 if old.description != self.description:
393393 changes.append('Description updated:')
394394 changes.append('\n'.join(
395- patience.unified_diff(
395+ difflib.unified_diff(
396396 a=old.description.split('\n'),
397397 b=self.description.split('\n'),
398398 fromfile='description-old',
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -1241,8 +1241,9 @@ class TrackerAdminController(DefaultAdminController):
12411241 self.app.globals.closed_status_names=post_data['closed_status_names']
12421242 custom_fields = post_data.get('custom_fields', [])
12431243 for field in custom_fields:
1244- field['name'] = '_' + '_'.join([
1245- w for w in NONALNUM_RE.split(field['label'].lower()) if w])
1244+ if 'name' not in field or not field['name']:
1245+ field['name'] = '_' + '_'.join([
1246+ w for w in NONALNUM_RE.split(field['label'].lower()) if w])
12461247 if field['type'] == 'milestone':
12471248 field.setdefault('milestones', [])
12481249
--- a/ForgeTracker/forgetracker/widgets/admin_custom_fields.py
+++ b/ForgeTracker/forgetracker/widgets/admin_custom_fields.py
@@ -81,6 +81,7 @@ class CustomFieldAdmin(ew.CompoundField):
8181 yield ew.JSLink('tracker_js/custom-fields.js')
8282
8383 fields = [
84+ ew.HiddenField(name='name'),
8485 ew.TextField(name='label'),
8586 ew.Checkbox(
8687 name='show_in_search',
--- a/ForgeWiki/forgewiki/model/wiki.py
+++ b/ForgeWiki/forgewiki/model/wiki.py
@@ -1,4 +1,5 @@
11 import pylons
2+import difflib
23 pylons.c = pylons.tmpl_context
34 pylons.g = pylons.app_globals
45 from pylons import g #g is a namespace for globally accessable app helpers
@@ -11,7 +12,6 @@ from ming.orm.declarative import MappedClass
1112 from allura.model import VersionedArtifact, Snapshot, Feed, Thread, Post, User, BaseAttachment
1213 from allura.model import Notification, project_orm_session
1314 from allura.lib import helpers as h
14-from allura.lib import patience
1515 from allura.lib import utils
1616
1717 config = utils.ConfigProxy(
@@ -86,7 +86,7 @@ class Page(VersionedArtifact):
8686 v2 = self
8787 la = [ line + '\n' for line in v1.text.splitlines() ]
8888 lb = [ line + '\n' for line in v2.text.splitlines() ]
89- diff = ''.join(patience.unified_diff(
89+ diff = ''.join(difflib.unified_diff(
9090 la, lb,
9191 'v%d' % v1.version,
9292 'v%d' % v2.version))
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -15,6 +15,7 @@ FormEncode==1.2.4
1515 # dep of Creoleparser
1616 Genshi==0.6
1717 # dep of oauth2
18+html2text==3.200.3
1819 httplib2==0.7.4
1920 iso8601==0.1.4
2021 Jinja2==2.6
@@ -39,6 +40,7 @@ pytidylib==0.2.1
3940 textile==2.1.5
4041 # dep of colander
4142 translationstring==0.4
43+TimerMiddleware==0.2.1
4244 TurboGears2==2.1.3
4345 # part of the stdlib, but with a version number. see http://guide.python-distribute.org/pip.html#listing-installed-packages
4446 wsgiref==0.1.2
--- a/requirements-sf.txt
+++ b/requirements-sf.txt
@@ -14,7 +14,6 @@ sqlalchemy-migrate==0.7.1
1414 pyzmq==2.1.7
1515
1616 # for the migration scripts only
17-html2text==3.101
1817 postmarkup==1.1.4
1918 # suds needed for teamforge import script
2019 suds==0.4
--- a/scripts/migrations/010-fix-home-permissions.py
+++ b/scripts/migrations/010-fix-home-permissions.py
@@ -7,6 +7,7 @@ from ming.orm import session
77 from bson import ObjectId
88
99 from allura import model as M
10+from allura.lib import utils
1011 from forgewiki.wiki_main import ForgeWikiApp
1112
1213 log = logging.getLogger('fix-home-permissions')
@@ -22,8 +23,9 @@ def main():
2223 else:
2324 log.info('Fixing permissions for all Home Wikis')
2425
25- for some_projects in chunked_project_iterator({'neighborhood_id': {'$nin': [ObjectId('4be2faf8898e33156f00003e'), # /u
26- ObjectId('4dbf2563bfc09e6362000005')]}}): # /motorola
26+ for some_projects in utils.chunked_find(M.Project, {'neighborhood_id': {
27+ '$nin': [ObjectId('4be2faf8898e33156f00003e'), # /u
28+ ObjectId('4dbf2563bfc09e6362000005')]}}): # /motorola
2729 for project in some_projects:
2830 c.project = project
2931 home_app = project.app_instance('home')
@@ -56,21 +58,6 @@ def main():
5658 home_app.config.acl = map(dict, new_acl.values())
5759 session(home_app.config).flush()
5860
59-PAGESIZE=1024
60-
61-def chunked_project_iterator(q_project):
62- '''shamelessly copied from refresh-all-repos.py'''
63- page = 0
64- while True:
65- results = (M.Project.query
66- .find(q_project)
67- .skip(PAGESIZE*page)
68- .limit(PAGESIZE)
69- .all())
70- if not results: break
71- yield results
72- page += 1
73-
7461 def project_role(project, name):
7562 role = M.ProjectRole.query.get(project_id=project._id, name=name)
7663 if role is None:
--- a/scripts/migrations/011-fix-subroles.py
+++ b/scripts/migrations/011-fix-subroles.py
@@ -12,11 +12,11 @@ For project.users:
1212 import sys
1313 import logging
1414
15-from pylons import c
1615 from ming.orm import session
1716 from ming.orm.ormsession import ThreadLocalORMSession
1817
1918 from allura import model as M
19+from allura.lib import utils
2020
2121 log = logging.getLogger('fix-subroles')
2222 log.addHandler(logging.StreamHandler(sys.stdout))
@@ -27,7 +27,7 @@ def main():
2727 log.info('Examining subroles in all non-user projects.')
2828 n_users = M.Neighborhood.query.get(name='Users')
2929 project_filter = dict(neighborhood_id={'$ne':n_users._id})
30- for some_projects in chunked_project_iterator(project_filter):
30+ for some_projects in utils.chunked_find(M.Project, project_filter):
3131 for project in some_projects:
3232 project_name = '%s.%s' % (project.neighborhood.name, project.shortname)
3333 project_roles = {}
@@ -53,7 +53,7 @@ def main():
5353 for user in project.users():
5454 pr = user.project_role(project=project)
5555 if not pr.roles: continue
56- for parent, children in [('Admin', ('Developer', 'Member')),
56+ for parent, children in [('Admin', ('Developer', 'Member')),
5757 ('Developer', ('Member',))]:
5858 if project_roles[parent]._id not in pr.roles: continue
5959 for role_name in children:
@@ -73,21 +73,5 @@ def main():
7373
7474 log.info('%s projects examined.' % num_projects_examined)
7575
76-
77-PAGESIZE=1024
78-
79-def chunked_project_iterator(q_project):
80- '''shamelessly copied from refresh-all-repos.py'''
81- page = 0
82- while True:
83- results = (M.Project.query
84- .find(q_project)
85- .skip(PAGESIZE*page)
86- .limit(PAGESIZE)
87- .all())
88- if not results: break
89- yield results
90- page += 1
91-
9276 if __name__ == '__main__':
9377 main()
--- a/scripts/migrations/012-uninstall-home.py
+++ b/scripts/migrations/012-uninstall-home.py
@@ -7,6 +7,7 @@ from bson import ObjectId
77 from mock import Mock, patch
88
99 from allura.lib import helpers as h
10+from allura.lib import utils
1011 from allura import model as M
1112 from forgewiki import model as WM
1213 from allura.ext.project_home import ProjectHomeApp
@@ -21,7 +22,8 @@ def main():
2122 possibly_orphaned_projects = 0
2223 solr_delete = Mock()
2324 notification_post = Mock()
24- for some_projects in chunked_project_iterator({'neighborhood_id': {'$ne': ObjectId("4be2faf8898e33156f00003e")}}):
25+ for some_projects in utils.chunked_find(M.Project, {'neighborhood_id': {
26+ '$ne': ObjectId("4be2faf8898e33156f00003e")}}):
2527 for project in some_projects:
2628 c.project = project
2729 old_home_app = project.app_instance('home')
@@ -102,20 +104,5 @@ def main():
102104 assert solr_delete.call_count == affected_projects, solr_delete.call_count
103105 assert notification_post.call_count == 2 * affected_projects, notification_post.call_count
104106
105-PAGESIZE=1024
106-
107-def chunked_project_iterator(q_project):
108- '''shamelessly copied from refresh-all-repos.py'''
109- page = 0
110- while True:
111- results = (M.Project.query
112- .find(q_project)
113- .skip(PAGESIZE*page)
114- .limit(PAGESIZE)
115- .all())
116- if not results: break
117- yield results
118- page += 1
119-
120107 if __name__ == '__main__':
121108 main()
--- a/scripts/migrations/013-update-ordinals.py
+++ b/scripts/migrations/013-update-ordinals.py
@@ -6,6 +6,7 @@ from ming.orm import session
66 from ming.orm.ormsession import ThreadLocalORMSession
77
88 from allura import model as M
9+from allura.lib import utils
910
1011 log = logging.getLogger('update-ordinals')
1112 log.addHandler(logging.StreamHandler(sys.stdout))
@@ -14,7 +15,7 @@ def main():
1415 test = sys.argv[-1] == 'test'
1516 num_projects_examined = 0
1617 log.info('Examining all projects for mount order.')
17- for some_projects in chunked_project_iterator({}):
18+ for some_projects in utils.chunked_find(M.Project):
1819 for project in some_projects:
1920 c.project = project
2021 mounts = project.ordered_mounts(include_search=True)
@@ -47,21 +48,5 @@ def main():
4748 ThreadLocalORMSession.flush_all()
4849 ThreadLocalORMSession.close_all()
4950
50-
51-PAGESIZE=1024
52-
53-def chunked_project_iterator(q_project):
54- '''shamelessly copied from refresh-all-repos.py'''
55- page = 0
56- while True:
57- results = (M.Project.query
58- .find(q_project)
59- .skip(PAGESIZE*page)
60- .limit(PAGESIZE)
61- .all())
62- if not results: break
63- yield results
64- page += 1
65-
6651 if __name__ == '__main__':
6752 main()
--- a/scripts/migrations/023-migrate-custom-profile-text.py
+++ /dev/null
@@ -1,56 +0,0 @@
1-import logging
2-import re
3-
4-from pylons import c
5-
6-from ming.orm import ThreadLocalORMSession
7-
8-from allura import model as M
9-from forgewiki import model as WM
10-from forgewiki.wiki_main import ForgeWikiApp
11-
12-log = logging.getLogger(__name__)
13-
14-default_description = r'^\s*(?:You can edit this description in the admin page)?\s*$'
15-
16-default_personal_project_tmpl = ("This is the personal project of %s."
17- " This project is created automatically during user registration"
18- " as an easy place to store personal data that doesn't need its own"
19- " project such as cloned repositories.\n\n%s")
20-
21-def main():
22- for p in M.Project.query.find().all():
23- user = p.user_project_of
24- if not user:
25- continue
26-
27- description = p.description
28- if description is None or re.match(default_description, description):
29- continue
30-
31- app = p.app_instance('wiki')
32- if app is None:
33- p.install_app('wiki')
34-
35- page = WM.Page.query.get(app_config_id=app.config._id, title='Home')
36- if page is None:
37- continue
38-
39- c.app = app
40- c.project = p
41- c.user = user
42-
43- if "This is the personal project of" in page.text:
44- if description not in page.text:
45- page.text = "%s\n\n%s" % (page.text, description)
46- log.info("Update wiki home page text for %s" % user.username)
47- elif "This is the default page" in page.text:
48- page.text = default_personal_project_tmpl % (user.display_name, description)
49- log.info("Update wiki home page text for %s" % user.username)
50- else:
51- pass
52-
53- ThreadLocalORMSession.flush_all()
54-
55-if __name__ == '__main__':
56- main()
--- /dev/null
+++ b/scripts/migrations/024-migrate-custom-profile-text.py
@@ -0,0 +1,59 @@
1+import logging
2+import re
3+
4+from pylons import c
5+
6+from ming.orm import ThreadLocalORMSession
7+
8+from allura import model as M
9+from allura.lib import utils
10+from forgewiki import model as WM
11+from forgewiki.wiki_main import ForgeWikiApp
12+
13+log = logging.getLogger(__name__)
14+
15+default_description = r'^\s*(?:You can edit this description in the admin page)?\s*$'
16+
17+default_personal_project_tmpl = ("This is the personal project of %s."
18+ " This project is created automatically during user registration"
19+ " as an easy place to store personal data that doesn't need its own"
20+ " project such as cloned repositories.\n\n%s")
21+
22+def main():
23+ users = M.Neighborhood.query.get(name='Users')
24+ for chunk in utils.chunked_find(M.Project, {'neighborhood_id': users._id}):
25+ for p in chunk:
26+ user = p.user_project_of
27+ if not user:
28+ continue
29+
30+ description = p.description
31+ if description is None or re.match(default_description, description):
32+ continue
33+
34+ app = p.app_instance('wiki')
35+ if app is None:
36+ p.install_app('wiki')
37+
38+ page = WM.Page.query.get(app_config_id=app.config._id, title='Home')
39+ if page is None:
40+ continue
41+
42+ c.app = app
43+ c.project = p
44+ c.user = user
45+
46+ if "This is the personal project of" in page.text:
47+ if description not in page.text:
48+ page.text = "%s\n\n%s" % (page.text, description)
49+ log.info("Update wiki home page text for %s" % user.username)
50+ elif "This is the default page" in page.text:
51+ page.text = default_personal_project_tmpl % (user.display_name, description)
52+ log.info("Update wiki home page text for %s" % user.username)
53+ else:
54+ pass
55+
56+ ThreadLocalORMSession.flush_all()
57+
58+if __name__ == '__main__':
59+ main()
--- a/scripts/refresh-all-repos.py
+++ b/scripts/refresh-all-repos.py
@@ -1,11 +1,11 @@
11 import logging
22 import optparse
3-from collections import defaultdict
43
54 from pylons import c
65 from ming.orm import ThreadLocalORMSession
76
87 from allura import model as M
8+from allura.lib import utils
99
1010 log = logging.getLogger(__name__)
1111
@@ -45,7 +45,7 @@ def main():
4545 M.repo.DiffInfoDoc.m.remove({})
4646 M.repo.LastCommitDoc.m.remove({})
4747 M.repo.CommitRunDoc.m.remove({})
48- for chunk in chunked_project_iterator(q_project):
48+ for chunk in utils.chunked_find(M.Project, q_project):
4949 for p in chunk:
5050 c.project = p
5151 if projects:
@@ -73,18 +73,5 @@ def main():
7373 ThreadLocalORMSession.flush_all()
7474 ThreadLocalORMSession.close_all()
7575
76-def chunked_project_iterator(q_project):
77- page = 0
78- while True:
79- results = (M.Project.query
80- .find(q_project)
81- .skip(PAGESIZE*page)
82- .limit(PAGESIZE)
83- .all())
84- if not results: break
85- yield results
86- page += 1
87-
88-
8976 if __name__ == '__main__':
9077 main()