You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by he...@apache.org on 2015/05/29 22:40:42 UTC
[20/45] allura git commit: [#7878] Used 2to3 to see what issues would
come up
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/model/monq_model.py
----------------------------------------------------------------------
diff --git a/model/monq_model.py b/model/monq_model.py
new file mode 100644
index 0000000..4aa49c1
--- /dev/null
+++ b/model/monq_model.py
@@ -0,0 +1,297 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import sys
+import time
+import traceback
+import logging
+from datetime import datetime, timedelta
+
+import pymongo
+from pylons import tmpl_context as c, app_globals as g
+from tg import config
+from paste.deploy.converters import asbool
+
+import ming
+from ming.utils import LazyProperty
+from ming import schema as S
+from ming.orm import session, FieldProperty
+from ming.orm.declarative import MappedClass
+
+from allura.lib.helpers import log_output, null_contextmanager
+from .session import task_orm_session
+
+log = logging.getLogger(__name__)
+
+
+class MonQTask(MappedClass):
+
+ '''Task to be executed by the taskd daemon.
+
+ Properties
+
+ - _id - bson.ObjectId() for this task
+ - state - 'ready', 'busy', 'error', 'complete', or 'skipped' task status
+ - priority - integer priority, higher is more priority
+ - result_type - either 'keep' or 'forget', what to do with the task when
+ it's done
+ - time_queue - time the task was queued
+ - time_start - time taskd began working on the task
+ - time_stop - time taskd stopped working on the task
+ - task_name - full dotted name of the task function to run
+ - process - identifier for which taskd process is working on the task
+ - context - values used to set c.project, c.app, c.user for the task
+ - args - ``*args`` to be sent to the task function
+ - kwargs - ``**kwargs`` to be sent to the task function
+ - result - if the task is complete, the return value. If in error, the traceback.
+ '''
+ states = ('ready', 'busy', 'error', 'complete', 'skipped')
+ result_types = ('keep', 'forget')
+
+ class __mongometa__:
+ session = task_orm_session
+ name = 'monq_task'
+ indexes = [
+ [
+ # used in MonQTask.get() method
+ # also 'state' queries exist in several other methods
+ ('state', ming.ASCENDING),
+ ('priority', ming.DESCENDING),
+ ('time_queue', ming.ASCENDING)
+ ],
+ [
+ # used by repo tarball status check, etc
+ 'state', 'task_name', 'time_queue'
+ ],
+ ]
+
+ _id = FieldProperty(S.ObjectId)
+ state = FieldProperty(S.OneOf(*states))
+ priority = FieldProperty(int)
+ result_type = FieldProperty(S.OneOf(*result_types))
+ time_queue = FieldProperty(datetime, if_missing=datetime.utcnow)
+ time_start = FieldProperty(datetime, if_missing=None)
+ time_stop = FieldProperty(datetime, if_missing=None)
+
+ task_name = FieldProperty(str)
+ process = FieldProperty(str)
+ context = FieldProperty(dict(
+ project_id=S.ObjectId,
+ app_config_id=S.ObjectId,
+ user_id=S.ObjectId,
+ notifications_disabled=bool))
+ args = FieldProperty([])
+ kwargs = FieldProperty({None: None})
+ result = FieldProperty(None, if_missing=None)
+
+ def __repr__(self):
+ from allura import model as M
+ project = M.Project.query.get(_id=self.context.project_id)
+ app = None
+ if project:
+ app_config = M.AppConfig.query.get(_id=self.context.app_config_id)
+ if app_config:
+ app = project.app_instance(app_config)
+ user = M.User.query.get(_id=self.context.user_id)
+ project_url = project and project.url() or None
+ app_mount = app and app.config.options.mount_point or None
+ username = user and user.username or None
+ return '<%s %s (%s) P:%d %s %s project:%s app:%s user:%s>' % (
+ self.__class__.__name__,
+ self._id,
+ self.state,
+ self.priority,
+ self.task_name,
+ self.process,
+ project_url,
+ app_mount,
+ username)
+
+ @LazyProperty
+ def function(self):
+ '''The function that is called by this task'''
+ smod, sfunc = self.task_name.rsplit('.', 1)
+ cur = __import__(smod, fromlist=[sfunc])
+ return getattr(cur, sfunc)
+
+ @classmethod
+ def post(cls,
+ function,
+ args=None,
+ kwargs=None,
+ result_type='forget',
+ priority=10,
+ delay=0):
+ '''Create a new task object based on the current context.'''
+ if args is None:
+ args = ()
+ if kwargs is None:
+ kwargs = {}
+ task_name = '%s.%s' % (
+ function.__module__,
+ function.__name__)
+ context = dict(
+ project_id=None,
+ app_config_id=None,
+ user_id=None,
+ notifications_disabled=False)
+ if getattr(c, 'project', None):
+ context['project_id'] = c.project._id
+ context[
+ 'notifications_disabled'] = c.project.notifications_disabled
+ if getattr(c, 'app', None):
+ context['app_config_id'] = c.app.config._id
+ if getattr(c, 'user', None):
+ context['user_id'] = c.user._id
+ obj = cls(
+ state='ready',
+ priority=priority,
+ result_type=result_type,
+ task_name=task_name,
+ args=args,
+ kwargs=kwargs,
+ process=None,
+ result=None,
+ context=context,
+ time_queue=datetime.utcnow() + timedelta(seconds=delay))
+ session(obj).flush(obj)
+ return obj
+
+ @classmethod
+ def get(cls, process='worker', state='ready', waitfunc=None, only=None):
+ '''Get the highest-priority, oldest, ready task and lock it to the
+ current process. If no task is available and waitfunc is supplied, call
+ the waitfunc before trying to get the task again. If waitfunc is None
+ and no tasks are available, return None. If waitfunc raises a
+ StopIteration, stop waiting for a task
+ '''
+ sort = [
+ ('priority', ming.DESCENDING),
+ ('time_queue', ming.ASCENDING)]
+ while True:
+ try:
+ query = dict(state=state)
+ query['time_queue'] = {'$lte': datetime.utcnow()}
+ if only:
+ query['task_name'] = {'$in': only}
+ obj = cls.query.find_and_modify(
+ query=query,
+ update={
+ '$set': dict(
+ state='busy',
+ process=process)
+ },
+ new=True,
+ sort=sort)
+ if obj is not None:
+ return obj
+ except pymongo.errors.OperationFailure as exc:
+ if 'No matching object found' not in exc.args[0]:
+ raise
+ if waitfunc is None:
+ return None
+ try:
+ waitfunc()
+ except StopIteration:
+ return None
+
+ @classmethod
+ def timeout_tasks(cls, older_than):
+ '''Mark all busy tasks older than a certain datetime as 'ready' again.
+ Used to retry 'stuck' tasks.'''
+ spec = dict(state='busy')
+ spec['time_start'] = {'$lt': older_than}
+ cls.query.update(spec, {'$set': dict(state='ready')}, multi=True)
+
+ @classmethod
+ def clear_complete(cls):
+ '''Delete the task objects for complete tasks'''
+ spec = dict(state='complete')
+ cls.query.remove(spec)
+
+ @classmethod
+ def run_ready(cls, worker=None):
+ '''Run all the tasks that are currently ready'''
+ i = 0
+ for i, task in enumerate(cls.query.find(dict(state='ready')).all()):
+ task.process = worker
+ task()
+ return i
+
+ def __call__(self, restore_context=True, nocapture=False):
+ '''Call the task function with its context. If restore_context is True,
+ c.project/app/user will be restored to the values they had before this
+ function was called.
+ '''
+ from allura import model as M
+ self.time_start = datetime.utcnow()
+ session(self).flush(self)
+ log.info('starting %r', self)
+ old_cproject = getattr(c, 'project', None)
+ old_capp = getattr(c, 'app', None)
+ old_cuser = getattr(c, 'user', None)
+ try:
+ func = self.function
+ c.project = M.Project.query.get(_id=self.context.project_id)
+ c.app = None
+ if c.project:
+ c.project.notifications_disabled = self.context.get(
+ 'notifications_disabled', False)
+ app_config = M.AppConfig.query.get(
+ _id=self.context.app_config_id)
+ if app_config:
+ c.app = c.project.app_instance(app_config)
+ c.user = M.User.query.get(_id=self.context.user_id)
+ with null_contextmanager() if nocapture else log_output(log):
+ self.result = func(*self.args, **self.kwargs)
+ self.state = 'complete'
+ return self.result
+ except Exception as exc:
+ if asbool(config.get('monq.raise_errors')):
+ raise
+ else:
+ log.exception('Error "%s" on job %s', exc, self)
+ self.state = 'error'
+ if hasattr(exc, 'format_error'):
+ self.result = exc.format_error()
+ log.error(self.result)
+ else:
+ self.result = traceback.format_exc()
+ finally:
+ self.time_stop = datetime.utcnow()
+ session(self).flush(self)
+ if restore_context:
+ c.project = old_cproject
+ c.app = old_capp
+ c.user = old_cuser
+
+ def join(self, poll_interval=0.1):
+ '''Wait until this task is either complete or errors out, then return the result.'''
+ while self.state not in ('complete', 'error'):
+ time.sleep(poll_interval)
+ self.query.find(dict(_id=self._id), refresh=True).first()
+ return self.result
+
+ @classmethod
+ def list(cls, state='ready'):
+ '''Print all tasks of a certain status to sys.stdout. Used for debugging.'''
+ for t in cls.query.find(dict(state=state)):
+ sys.stdout.write('%r\n' % t)
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/model/neighborhood.py
----------------------------------------------------------------------
diff --git a/model/neighborhood.py b/model/neighborhood.py
new file mode 100644
index 0000000..8ee7173
--- /dev/null
+++ b/model/neighborhood.py
@@ -0,0 +1,295 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import re
+import json
+import logging
+from collections import OrderedDict
+
+from ming import schema as S
+from ming.orm import FieldProperty, RelationProperty
+from ming.orm.declarative import MappedClass
+from ming.utils import LazyProperty
+
+from pylons import request
+from pylons import tmpl_context as c, app_globals as g
+
+from allura.lib import plugin
+
+from .session import main_orm_session
+from .filesystem import File
+from .types import MarkdownCache
+
+log = logging.getLogger(__name__)
+
+
+class NeighborhoodFile(File):
+
+ class __mongometa__:
+ session = main_orm_session
+ indexes = ['neighborhood_id']
+
+ neighborhood_id = FieldProperty(S.ObjectId)
+
+re_picker_css_type = re.compile('^/\*(.+)\*/')
+re_font_project_title = re.compile('font-family:(.+);\}')
+re_color_project_title = re.compile('color:(.+);\}')
+re_bgcolor_barontop = re.compile('background\-color:([^;}]+);')
+re_bgcolor_titlebar = re.compile('background\-color:([^;}]+);')
+re_color_titlebar = re.compile('color:([^;}]+);')
+re_icon_theme = re.compile('neo-icon-set-(ffffff|454545)-256x350.png')
+
+
+class Neighborhood(MappedClass):
+
+ '''Provide a grouping of related projects.
+
+ url_prefix - location of neighborhood (may include scheme and/or host)
+ css - block of CSS text to add to all neighborhood pages
+ '''
+ class __mongometa__:
+ session = main_orm_session
+ name = 'neighborhood'
+ unique_indexes = ['url_prefix']
+
+ _id = FieldProperty(S.ObjectId)
+ name = FieldProperty(str)
+ # e.g. http://adobe.openforge.com/ or projects/
+ url_prefix = FieldProperty(str)
+ shortname_prefix = FieldProperty(str, if_missing='')
+ css = FieldProperty(str, if_missing='')
+ homepage = FieldProperty(str, if_missing='')
+ homepage_cache = FieldProperty(MarkdownCache)
+ redirect = FieldProperty(str, if_missing='')
+ projects = RelationProperty('Project')
+ allow_browse = FieldProperty(bool, if_missing=True)
+ show_title = FieldProperty(bool, if_missing=True)
+ site_specific_html = FieldProperty(str, if_missing='')
+ project_template = FieldProperty(str, if_missing='')
+ tracking_id = FieldProperty(str, if_missing='')
+ project_list_url = FieldProperty(str, if_missing='')
+ level = FieldProperty(S.Deprecated)
+ allow_private = FieldProperty(S.Deprecated)
+ features = FieldProperty(dict(
+ private_projects=bool,
+ max_projects=S.Int,
+ css=str,
+ google_analytics=bool))
+ anchored_tools = FieldProperty(str, if_missing='')
+ prohibited_tools = FieldProperty(str, if_missing='')
+
+ def parent_security_context(self):
+ return None
+
+ @LazyProperty
+ def neighborhood_project(self):
+ from .project import Project
+ p = Project.query.get(
+ neighborhood_id=self._id,
+ is_nbhd_project=True)
+ assert p
+ return p
+
+ @property
+ def acl(self):
+ return self.neighborhood_project.acl
+
+ def url(self):
+ url = self.url_prefix
+ if url.startswith('//'):
+ try:
+ return request.scheme + ':' + url
+ except TypeError: # pragma no cover
+ return 'http:' + url
+ else:
+ return url
+
+ def register_project(self, shortname, user=None, project_name=None, user_project=False, private_project=False, apps=None):
+ '''Register a new project in the neighborhood. The given user will
+ become the project's superuser. If no user is specified, c.user is used.
+ '''
+ provider = plugin.ProjectRegistrationProvider.get()
+ if project_name is None:
+ project_name = shortname
+ return provider.register_project(
+ self, shortname, project_name, user or getattr(c, 'user', None), user_project, private_project, apps)
+
+ def bind_controller(self, controller):
+ from allura.controllers.project import NeighborhoodController
+ controller_attr = self.url_prefix[1:-1]
+ setattr(controller, controller_attr, NeighborhoodController(self))
+
+ def get_custom_css(self):
+ if self.allow_custom_css:
+ return self.css
+ return ""
+
+ @property
+ def has_home_tool(self):
+ return (self.neighborhood_project
+ .app_config_by_tool_type('home') is not None)
+
+ @property
+ def icon(self):
+ return NeighborhoodFile.query.get(neighborhood_id=self._id)
+
+ @property
+ def allow_custom_css(self):
+ return self.features['css'] == 'custom' or self.features['css'] == 'picker'
+
+ def get_project_template(self):
+ if self.project_template:
+ return json.loads(self.project_template)
+ return {}
+
+ def get_max_projects(self):
+ return self.features['max_projects']
+
+ def get_css_for_picker(self):
+ projecttitlefont = {'label': 'Project title, font',
+ 'name': 'projecttitlefont', 'value': '', 'type': 'font'}
+ projecttitlecolor = {'label': 'Project title, color',
+ 'name': 'projecttitlecolor', 'value': '', 'type': 'color'}
+ barontop = {'label': 'Bar on top', 'name':
+ 'barontop', 'value': '', 'type': 'color'}
+ titlebarbackground = {'label': 'Title bar, background',
+ 'name': 'titlebarbackground', 'value': '', 'type': 'color'}
+ titlebarcolor = {
+ 'label': 'Title bar, foreground', 'name': 'titlebarcolor', 'value': '', 'type': 'color',
+ 'additional': """<label>Icons theme:</label> <select name="css-addopt-icon-theme" class="add_opt">
+ <option value="default">default</option>
+ <option value="dark"%(titlebarcolor_dark)s>dark</option>
+ <option value="white"%(titlebarcolor_white)s>white</option>
+ </select>"""}
+ titlebarcolor_dark = ''
+ titlebarcolor_white = ''
+
+ if self.css is not None:
+ for css_line in self.css.split('\n'):
+ m = re_picker_css_type.search(css_line)
+ if not m:
+ continue
+
+ css_type = m.group(1)
+ if css_type == "projecttitlefont":
+ m = re_font_project_title.search(css_line)
+ if m:
+ projecttitlefont['value'] = m.group(1)
+
+ elif css_type == "projecttitlecolor":
+ m = re_color_project_title.search(css_line)
+ if m:
+ projecttitlecolor['value'] = m.group(1)
+
+ elif css_type == "barontop":
+ m = re_bgcolor_barontop.search(css_line)
+ if m:
+ barontop['value'] = m.group(1)
+
+ elif css_type == "titlebarbackground":
+ m = re_bgcolor_titlebar.search(css_line)
+ if m:
+ titlebarbackground['value'] = m.group(1)
+
+ elif css_type == "titlebarcolor":
+ m = re_color_titlebar.search(css_line)
+ if m:
+ titlebarcolor['value'] = m.group(1)
+ m = re_icon_theme.search(css_line)
+ if m:
+ icon_theme = m.group(1)
+ if icon_theme == "ffffff":
+ titlebarcolor_dark = ' selected="selected"'
+ elif icon_theme == "454545":
+ titlebarcolor_white = ' selected="selected"'
+
+ titlebarcolor[
+ 'additional'] = titlebarcolor['additional'] % {'titlebarcolor_dark': titlebarcolor_dark,
+ 'titlebarcolor_white': titlebarcolor_white}
+
+ styles_list = []
+ styles_list.append(projecttitlefont)
+ styles_list.append(projecttitlecolor)
+ styles_list.append(barontop)
+ styles_list.append(titlebarbackground)
+ styles_list.append(titlebarcolor)
+ return styles_list
+
+ @staticmethod
+ def compile_css_for_picker(css_form_dict):
+ # Check css values
+ for key in list(css_form_dict.keys()):
+ if ';' in css_form_dict[key] or '}' in css_form_dict[key]:
+ css_form_dict[key] = ''
+
+ css_text = ""
+ if 'projecttitlefont' in css_form_dict and css_form_dict['projecttitlefont'] != '':
+ css_text += "/*projecttitlefont*/.project_title{font-family:%s;}\n" % (
+ css_form_dict['projecttitlefont'])
+
+ if 'projecttitlecolor' in css_form_dict and css_form_dict['projecttitlecolor'] != '':
+ css_text += "/*projecttitlecolor*/.project_title{color:%s;}\n" % (
+ css_form_dict['projecttitlecolor'])
+
+ if 'barontop' in css_form_dict and css_form_dict['barontop'] != '':
+ css_text += "/*barontop*/.pad h2.colored {background-color:%(bgcolor)s; background-image: none;}\n" % \
+ {'bgcolor': css_form_dict['barontop']}
+
+ if 'titlebarbackground' in css_form_dict and css_form_dict['titlebarbackground'] != '':
+ css_text += "/*titlebarbackground*/.pad h2.title{background-color:%(bgcolor)s; background-image: none;}\n" % \
+ {'bgcolor': css_form_dict['titlebarbackground']}
+
+ if 'titlebarcolor' in css_form_dict and css_form_dict['titlebarcolor'] != '':
+ icon_theme = ''
+ if 'addopt-icon-theme' in css_form_dict:
+ if css_form_dict['addopt-icon-theme'] == "dark":
+ icon_theme = ".pad h2.dark small b.ico {background-image: url('%s%s');}" % (
+ g.theme_href(''),
+ 'images/neo-icon-set-ffffff-256x350.png')
+ elif css_form_dict['addopt-icon-theme'] == "white":
+ icon_theme = ".pad h2.dark small b.ico {background-image: url('%s%s');}" % (
+ g.theme_href(''),
+ 'images/neo-icon-set-454545-256x350.png')
+
+ css_text += "/*titlebarcolor*/.pad h2.title, .pad h2.title small a {color:%s;} %s\n" % (
+ css_form_dict['titlebarcolor'], icon_theme)
+
+ return css_text
+
+ def migrate_css_for_picker(self):
+ self.css = ""
+
+ def get_anchored_tools(self):
+ if not self.anchored_tools:
+ return dict()
+ try:
+ anchored_tools = [at.strip()
+ for at in self.anchored_tools.split(',')]
+ return OrderedDict((tool.split(':')[0].lower(), tool.split(':')[1]) for tool in anchored_tools)
+ except Exception:
+ log.warning("anchored_tools isn't valid", exc_info=True)
+ return dict()
+
+ def get_prohibited_tools(self):
+ prohibited_tools = []
+ if self.prohibited_tools:
+ prohibited_tools = [tool.lower().strip() for tool in self.prohibited_tools.split(',')]
+ return prohibited_tools
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/model/notification.py
----------------------------------------------------------------------
diff --git a/model/notification.py b/model/notification.py
new file mode 100644
index 0000000..84064bd
--- /dev/null
+++ b/model/notification.py
@@ -0,0 +1,721 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+'''Manage notifications and subscriptions
+
+When an artifact is modified:
+
+- Notification generated by tool app
+- Search is made for subscriptions matching the notification
+- Notification is added to each matching subscriptions' queue
+
+Periodically:
+
+- For each subscriptions with notifications and direct delivery:
+ - For each notification, enqueue as a separate email message
+ - Clear subscription's notification list
+- For each subscription with notifications and delivery due:
+ - Enqueue one email message with all notifications
+ - Clear subscription's notification list
+
+'''
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import logging
+from bson import ObjectId
+from datetime import datetime, timedelta
+from collections import defaultdict
+
+from pylons import tmpl_context as c, app_globals as g
+from tg import config
+import pymongo
+import jinja2
+from paste.deploy.converters import asbool
+
+from ming import schema as S
+from ming.orm import FieldProperty, ForeignIdProperty, RelationProperty, session
+from ming.orm.declarative import MappedClass
+
+from allura.lib import helpers as h
+from allura.lib import security
+from allura.lib.utils import take_while_true
+import allura.tasks.mail_tasks
+
+from .session import main_orm_session
+from .auth import User, AlluraUserProperty
+
+
+log = logging.getLogger(__name__)
+
+MAILBOX_QUIESCENT = None # Re-enable with [#1384]: timedelta(minutes=10)
+
+
+class Notification(MappedClass):
+
+ '''
+ Temporarily store notifications that will be emailed or displayed as a web flash.
+ This does not contain any recipient information.
+ '''
+
+ class __mongometa__:
+ session = main_orm_session
+ name = 'notification'
+ indexes = ['project_id']
+
+ _id = FieldProperty(str, if_missing=h.gen_message_id)
+
+ # Classify notifications
+ neighborhood_id = ForeignIdProperty(
+ 'Neighborhood', if_missing=lambda: c.project.neighborhood._id)
+ project_id = ForeignIdProperty('Project', if_missing=lambda: c.project._id)
+ app_config_id = ForeignIdProperty(
+ 'AppConfig', if_missing=lambda: c.app.config._id)
+ tool_name = FieldProperty(str, if_missing=lambda: c.app.config.tool_name)
+ ref_id = ForeignIdProperty('ArtifactReference')
+ topic = FieldProperty(str)
+
+ # Notification Content
+ in_reply_to = FieldProperty(str)
+ references = FieldProperty([str])
+ from_address = FieldProperty(str)
+ reply_to_address = FieldProperty(str)
+ subject = FieldProperty(str)
+ text = FieldProperty(str)
+ link = FieldProperty(str)
+ author_id = AlluraUserProperty()
+ feed_meta = FieldProperty(S.Deprecated)
+ artifact_reference = FieldProperty(S.Deprecated)
+ pubdate = FieldProperty(datetime, if_missing=datetime.utcnow)
+
+ ref = RelationProperty('ArtifactReference')
+
+ view = jinja2.Environment(
+ loader=jinja2.PackageLoader('allura', 'templates'),
+ auto_reload=asbool(config.get('auto_reload_templates', True)),
+ )
+
+ @classmethod
+ def post(cls, artifact, topic, **kw):
+ '''Create a notification and send the notify message'''
+ import allura.tasks.notification_tasks
+ n = cls._make_notification(artifact, topic, **kw)
+ if n:
+ # make sure notification is flushed in time for task to process it
+ session(n).flush(n)
+ allura.tasks.notification_tasks.notify.post(
+ n._id, artifact.index_id(), topic)
+ return n
+
+ @classmethod
+ def post_user(cls, user, artifact, topic, **kw):
+ '''Create a notification and deliver directly to a user's flash
+ mailbox'''
+ try:
+ mbox = Mailbox(user_id=user._id, is_flash=True,
+ project_id=None,
+ app_config_id=None)
+ session(mbox).flush(mbox)
+ except pymongo.errors.DuplicateKeyError:
+ session(mbox).expunge(mbox)
+ mbox = Mailbox.query.get(user_id=user._id, is_flash=True)
+ n = cls._make_notification(artifact, topic, **kw)
+ if n:
+ mbox.queue.append(n._id)
+ mbox.queue_empty = False
+ return n
+
+ @classmethod
+ def _make_notification(cls, artifact, topic, **kwargs):
+ '''
+ Create a Notification instance based on an artifact. Special handling
+ for comments when topic=='message'
+ '''
+
+ from allura.model import Project
+ idx = artifact.index() if artifact else None
+ subject_prefix = '[%s:%s] ' % (
+ c.project.shortname, c.app.config.options.mount_point)
+ post = ''
+ if topic == 'message':
+ post = kwargs.pop('post')
+ text = kwargs.get('text') or post.text
+ file_info = kwargs.pop('file_info', None)
+ if file_info is not None:
+ text = "%s\n\n\nAttachment:" % text
+ if not isinstance(file_info, list):
+ file_info = [file_info]
+ for attach in file_info:
+ attach.file.seek(0, 2)
+ bytecount = attach.file.tell()
+ attach.file.seek(0)
+ text = "%s %s (%s; %s) " % (
+ text, attach.filename, h.do_filesizeformat(bytecount), attach.type)
+
+ subject = post.subject or ''
+ if post.parent_id and not subject.lower().startswith('re:'):
+ subject = 'Re: ' + subject
+ author = post.author()
+ msg_id = artifact.url() + post._id
+ parent_msg_id = artifact.url() + \
+ post.parent_id if post.parent_id else artifact.message_id(
+ )
+ d = dict(
+ _id=msg_id,
+ from_address=str(
+ author._id) if author != User.anonymous() else None,
+ reply_to_address='"%s" <%s>' % (
+ subject_prefix, getattr(
+ artifact, 'email_address', g.noreply)),
+ subject=subject_prefix + subject,
+ text=text,
+ in_reply_to=parent_msg_id,
+ references=cls._references(artifact, post),
+ author_id=author._id,
+ pubdate=datetime.utcnow())
+ elif topic == 'flash':
+ n = cls(topic=topic,
+ text=kwargs['text'],
+ subject=kwargs.pop('subject', ''))
+ return n
+ else:
+ subject = kwargs.pop('subject', '%s modified by %s' % (
+ h.get_first(idx, 'title'), c.user.get_pref('display_name')))
+ reply_to = '"%s" <%s>' % (
+ h.get_first(idx, 'title'),
+ getattr(artifact, 'email_address', g.noreply))
+ d = dict(
+ from_address=reply_to,
+ reply_to_address=reply_to,
+ subject=subject_prefix + subject,
+ text=kwargs.pop('text', subject),
+ author_id=c.user._id,
+ pubdate=datetime.utcnow())
+ if kwargs.get('message_id'):
+ d['_id'] = kwargs['message_id']
+ if c.user.get_pref('email_address'):
+ d['from_address'] = '"%s" <%s>' % (
+ c.user.get_pref('display_name'),
+ c.user.get_pref('email_address'))
+ elif c.user.email_addresses:
+ d['from_address'] = '"%s" <%s>' % (
+ c.user.get_pref('display_name'),
+ c.user.email_addresses[0])
+ if not d.get('text'):
+ d['text'] = ''
+ try:
+ ''' Add addional text to the notification e-mail based on the artifact type '''
+ template = cls.view.get_template(
+ 'mail/' + artifact.type_s + '.txt')
+ d['text'] += template.render(dict(c=c, g=g,
+ config=config, data=artifact, post=post, h=h))
+ except jinja2.TemplateNotFound:
+ pass
+ except:
+ ''' Catch any errors loading or rendering the template,
+ but the notification still gets sent if there is an error
+ '''
+ log.warn('Could not render notification template %s' %
+ artifact.type_s, exc_info=True)
+
+ assert d['reply_to_address'] is not None
+ project = c.project
+ if d.get('project_id', c.project._id) != c.project._id:
+ project = Project.query.get(_id=d['project_id'])
+ if project.notifications_disabled:
+ log.debug(
+ 'Notifications disabled for project %s, not sending %s(%r)',
+ project.shortname, topic, artifact)
+ return None
+ n = cls(ref_id=artifact.index_id(),
+ topic=topic,
+ link=kwargs.pop('link', artifact.url()),
+ **d)
+ return n
+
+ def footer(self, toaddr=''):
+ return self.ref.artifact.get_mail_footer(self, toaddr)
+
+ def _sender(self):
+ from allura.model import AppConfig
+ app_config = AppConfig.query.get(_id=self.app_config_id)
+ app = app_config.project.app_instance(app_config)
+ return app.email_address if app else None
+
+ @classmethod
+ def _references(cls, artifact, post):
+ msg_ids = []
+ while post and post.parent_id:
+ msg_ids.append(artifact.url() + post.parent_id)
+ post = post.parent
+ msg_ids.append(artifact.message_id())
+ msg_ids.reverse()
+ return msg_ids
+
+ def send_simple(self, toaddr):
+ allura.tasks.mail_tasks.sendsimplemail.post(
+ toaddr=toaddr,
+ fromaddr=self.from_address,
+ reply_to=self.reply_to_address,
+ subject=self.subject,
+ sender=self._sender(),
+ message_id=self._id,
+ in_reply_to=self.in_reply_to,
+ references=self.references,
+ text=(self.text or '') + self.footer(toaddr))
+
+ def send_direct(self, user_id):
+ user = User.query.get(_id=ObjectId(user_id), disabled=False, pending=False)
+ artifact = self.ref.artifact
+ log.debug('Sending direct notification %s to user %s',
+ self._id, user_id)
+ # Don't send if user disabled
+ if not user:
+ log.debug("Skipping notification - enabled user %s not found" %
+ user_id)
+ return
+ # Don't send if user doesn't have read perms to the artifact
+ if user and artifact and \
+ not security.has_access(artifact, 'read', user)():
+ log.debug("Skipping notification - User %s doesn't have read "
+ "access to artifact %s" % (user_id, str(self.ref_id)))
+ log.debug("User roles [%s]; artifact ACL [%s]; PSC ACL [%s]",
+ ', '.join([str(r) for r in security.Credentials.get().user_roles(
+ user_id=user_id, project_id=artifact.project._id).reaching_ids]),
+ ', '.join([str(a) for a in artifact.acl]),
+ ', '.join([str(a) for a in artifact.parent_security_context().acl]))
+ return
+ allura.tasks.mail_tasks.sendmail.post(
+ destinations=[str(user_id)],
+ fromaddr=self.from_address,
+ reply_to=self.reply_to_address,
+ subject=self.subject,
+ message_id=self._id,
+ in_reply_to=self.in_reply_to,
+ references=self.references,
+ sender=self._sender(),
+ text=(self.text or '') + self.footer())
+
+ @classmethod
+ def send_digest(self, user_id, from_address, subject, notifications,
+ reply_to_address=None):
+ if not notifications:
+ return
+ user = User.query.get(_id=ObjectId(user_id), disabled=False, pending=False)
+ if not user:
+ log.debug("Skipping notification - enabled user %s not found " %
+ user_id)
+ return
+ # Filter out notifications for which the user doesn't have read
+ # permissions to the artifact.
+ artifact = self.ref.artifact
+
+ def perm_check(notification):
+ return not (user and artifact) or \
+ security.has_access(artifact, 'read', user)()
+ notifications = list(filter(perm_check, notifications))
+
+ log.debug('Sending digest of notifications [%s] to user %s', ', '.join(
+ [n._id for n in notifications]), user_id)
+ if reply_to_address is None:
+ reply_to_address = from_address
+ text = ['Digest of %s' % subject]
+ for n in notifications:
+ text.append('From: %s' % n.from_address)
+ text.append('Subject: %s' % (n.subject or '(no subject)'))
+ text.append('Message-ID: %s' % n._id)
+ text.append('')
+ text.append(n.text or '-no text-')
+ text.append(n.footer())
+ text = '\n'.join(text)
+ allura.tasks.mail_tasks.sendmail.post(
+ destinations=[str(user_id)],
+ fromaddr=from_address,
+ reply_to=reply_to_address,
+ subject=subject,
+ message_id=h.gen_message_id(),
+ text=text)
+
+ @classmethod
+ def send_summary(self, user_id, from_address, subject, notifications):
+ if not notifications:
+ return
+ log.debug('Sending summary of notifications [%s] to user %s', ', '.join(
+ [n._id for n in notifications]), user_id)
+ text = ['Digest of %s' % subject]
+ for n in notifications:
+ text.append('From: %s' % n.from_address)
+ text.append('Subject: %s' % (n.subject or '(no subject)'))
+ text.append('Message-ID: %s' % n._id)
+ text.append('')
+ text.append(h.text.truncate(n.text or '-no text-', 128))
+ text.append(n.footer())
+ text = '\n'.join(text)
+ allura.tasks.mail_tasks.sendmail.post(
+ destinations=[str(user_id)],
+ fromaddr=from_address,
+ reply_to=from_address,
+ subject=subject,
+ message_id=h.gen_message_id(),
+ text=text)
+
+
+class Mailbox(MappedClass):
+
+ '''
+ Holds a queue of notifications for an artifact, or a user (webflash messages)
+ for a subscriber.
+ FIXME: describe the Mailbox concept better.
+ '''
+
+ class __mongometa__:
+ session = main_orm_session
+ name = 'mailbox'
+ unique_indexes = [
+ ('user_id', 'project_id', 'app_config_id',
+ 'artifact_index_id', 'topic', 'is_flash'),
+ ]
+ indexes = [
+ ('project_id', 'artifact_index_id'),
+ ('is_flash', 'user_id'),
+ ('type', 'next_scheduled'), # for q_digest
+ ('type', 'queue_empty'), # for q_direct
+ # for deliver()
+ ('project_id', 'app_config_id', 'artifact_index_id', 'topic'),
+ ]
+
+ _id = FieldProperty(S.ObjectId)
+ user_id = AlluraUserProperty(if_missing=lambda: c.user._id)
+ project_id = ForeignIdProperty('Project', if_missing=lambda: c.project._id)
+ app_config_id = ForeignIdProperty(
+ 'AppConfig', if_missing=lambda: c.app.config._id)
+
+ # Subscription filters
+ artifact_title = FieldProperty(str)
+ artifact_url = FieldProperty(str)
+ artifact_index_id = FieldProperty(str)
+ topic = FieldProperty(str)
+
+ # Subscription type
+ is_flash = FieldProperty(bool, if_missing=False)
+ type = FieldProperty(S.OneOf('direct', 'digest', 'summary', 'flash'))
+ frequency = FieldProperty(dict(
+ n=int, unit=S.OneOf('day', 'week', 'month')))
+ next_scheduled = FieldProperty(datetime, if_missing=datetime.utcnow)
+ last_modified = FieldProperty(datetime, if_missing=datetime(2000, 1, 1))
+
+ # a list of notification _id values
+ queue = FieldProperty([str])
+ queue_empty = FieldProperty(bool)
+
+ project = RelationProperty('Project')
+ app_config = RelationProperty('AppConfig')
+
+ @classmethod
+ def subscribe(
+ cls,
+ user_id=None, project_id=None, app_config_id=None,
+ artifact=None, topic=None,
+ type='direct', n=1, unit='day'):
+ if user_id is None:
+ user_id = c.user._id
+ if project_id is None:
+ project_id = c.project._id
+ if app_config_id is None:
+ app_config_id = c.app.config._id
+ tool_already_subscribed = cls.query.get(user_id=user_id,
+ project_id=project_id,
+ app_config_id=app_config_id,
+ artifact_index_id=None)
+ if tool_already_subscribed:
+ return
+ if artifact is None:
+ artifact_title = 'All artifacts'
+ artifact_url = None
+ artifact_index_id = None
+ else:
+ i = artifact.index()
+ artifact_title = h.get_first(i, 'title')
+ artifact_url = artifact.url()
+ artifact_index_id = i['id']
+ artifact_already_subscribed = cls.query.get(user_id=user_id,
+ project_id=project_id,
+ app_config_id=app_config_id,
+ artifact_index_id=artifact_index_id)
+ if artifact_already_subscribed:
+ return
+ d = dict(
+ user_id=user_id, project_id=project_id, app_config_id=app_config_id,
+ artifact_index_id=artifact_index_id, topic=topic)
+ sess = session(cls)
+ try:
+ mbox = cls(
+ type=type, frequency=dict(n=n, unit=unit),
+ artifact_title=artifact_title,
+ artifact_url=artifact_url,
+ **d)
+ sess.flush(mbox)
+ except pymongo.errors.DuplicateKeyError:
+ sess.expunge(mbox)
+ mbox = cls.query.get(**d)
+ mbox.artifact_title = artifact_title
+ mbox.artifact_url = artifact_url
+ mbox.type = type
+ mbox.frequency.n = n
+ mbox.frequency.unit = unit
+ sess.flush(mbox)
+ if not artifact_index_id:
+ # Unsubscribe from individual artifacts when subscribing to the
+ # tool
+ for other_mbox in cls.query.find(dict(
+ user_id=user_id, project_id=project_id, app_config_id=app_config_id)):
+ if other_mbox is not mbox:
+ other_mbox.delete()
+
+ @classmethod
+ def unsubscribe(
+ cls,
+ user_id=None, project_id=None, app_config_id=None,
+ artifact_index_id=None, topic=None):
+ if user_id is None:
+ user_id = c.user._id
+ if project_id is None:
+ project_id = c.project._id
+ if app_config_id is None:
+ app_config_id = c.app.config._id
+ cls.query.remove(dict(
+ user_id=user_id,
+ project_id=project_id,
+ app_config_id=app_config_id,
+ artifact_index_id=artifact_index_id,
+ topic=topic))
+
+ @classmethod
+ def subscribed(
+ cls, user_id=None, project_id=None, app_config_id=None,
+ artifact=None, topic=None):
+ if user_id is None:
+ user_id = c.user._id
+ if project_id is None:
+ project_id = c.project._id
+ if app_config_id is None:
+ app_config_id = c.app.config._id
+ if artifact is None:
+ artifact_index_id = None
+ else:
+ i = artifact.index()
+ artifact_index_id = i['id']
+ return cls.query.find(dict(
+ user_id=user_id,
+ project_id=project_id,
+ app_config_id=app_config_id,
+ artifact_index_id=artifact_index_id)).count() != 0
+
+ @classmethod
+ def deliver(cls, nid, artifact_index_id, topic):
+ '''Called in the notification message handler to deliver notification IDs
+ to the appropriate mailboxes. Atomically appends the nids
+ to the appropriate mailboxes.
+ '''
+ d = {
+ 'project_id': c.project._id,
+ 'app_config_id': c.app.config._id,
+ 'artifact_index_id': {'$in': [None, artifact_index_id]},
+ 'topic': {'$in': [None, topic]}
+ }
+ mboxes = cls.query.find(d).all()
+ log.debug('Delivering notification %s to mailboxes [%s]', nid, ', '.join(
+ [str(m._id) for m in mboxes]))
+ for mbox in mboxes:
+ try:
+ mbox.query.update(
+ {'$push': dict(queue=nid),
+ '$set': dict(last_modified=datetime.utcnow(),
+ queue_empty=False),
+ })
+ # Make sure the mbox doesn't stick around to be flush()ed
+ session(mbox).expunge(mbox)
+ except:
+ # log error but try to keep processing, lest all the other eligible
+ # mboxes for this notification get skipped and lost forever
+ log.exception(
+ 'Error adding notification: %s for artifact %s on project %s to user %s',
+ nid, artifact_index_id, c.project._id, mbox.user_id)
+
+ @classmethod
+ def fire_ready(cls):
+ '''Fires all direct subscriptions with notifications as well as
+ all summary & digest subscriptions with notifications that are ready.
+ Clears the mailbox queue.
+ '''
+ now = datetime.utcnow()
+ # Queries to find all matching subscription objects
+ q_direct = dict(
+ type='direct',
+ queue_empty=False,
+ )
+ if MAILBOX_QUIESCENT:
+ q_direct['last_modified'] = {'$lt': now - MAILBOX_QUIESCENT}
+ q_digest = dict(
+ type={'$in': ['digest', 'summary']},
+ next_scheduled={'$lt': now})
+
+ def find_and_modify_direct_mbox():
+ return cls.query.find_and_modify(
+ query=q_direct,
+ update={'$set': dict(
+ queue=[],
+ queue_empty=True,
+ )},
+ new=False)
+
+ for mbox in take_while_true(find_and_modify_direct_mbox):
+ try:
+ mbox.fire(now)
+ except:
+ log.exception(
+ 'Error firing mbox: %s with queue: [%s]', str(mbox._id), ', '.join(mbox.queue))
+ # re-raise so we don't keep (destructively) trying to process
+ # mboxes
+ raise
+
+ for mbox in cls.query.find(q_digest):
+ next_scheduled = now
+ if mbox.frequency.unit == 'day':
+ next_scheduled += timedelta(days=mbox.frequency.n)
+ elif mbox.frequency.unit == 'week':
+ next_scheduled += timedelta(days=7 * mbox.frequency.n)
+ elif mbox.frequency.unit == 'month':
+ next_scheduled += timedelta(days=30 * mbox.frequency.n)
+ mbox = cls.query.find_and_modify(
+ query=dict(_id=mbox._id),
+ update={'$set': dict(
+ next_scheduled=next_scheduled,
+ queue=[],
+ queue_empty=True,
+ )},
+ new=False)
+ mbox.fire(now)
+
+ def fire(self, now):
+ '''
+ Send all notifications that this mailbox has enqueued.
+ '''
+ notifications = Notification.query.find(dict(_id={'$in': self.queue}))
+ notifications = notifications.all()
+ if len(notifications) != len(self.queue):
+ log.error('Mailbox queue error: Mailbox %s queued [%s], found [%s]', str(
+ self._id), ', '.join(self.queue), ', '.join([n._id for n in notifications]))
+ else:
+ log.debug('Firing mailbox %s notifications [%s], found [%s]', str(
+ self._id), ', '.join(self.queue), ', '.join([n._id for n in notifications]))
+ if self.type == 'direct':
+ ngroups = defaultdict(list)
+ for n in notifications:
+ try:
+ if n.topic == 'message':
+ n.send_direct(self.user_id)
+ # Messages must be sent individually so they can be replied
+ # to individually
+ else:
+ key = (n.subject, n.from_address,
+ n.reply_to_address, n.author_id)
+ ngroups[key].append(n)
+ except:
+ # log error but keep trying to deliver other notifications,
+ # lest the other notifications (which have already been removed
+ # from the mobx's queue in mongo) be lost
+ log.exception(
+ 'Error sending notification: %s to mbox %s (user %s)',
+ n._id, self._id, self.user_id)
+ # Accumulate messages from same address with same subject
+ for (subject, from_address, reply_to_address, author_id), ns in ngroups.items():
+ try:
+ if len(ns) == 1:
+ ns[0].send_direct(self.user_id)
+ else:
+ Notification.send_digest(
+ self.user_id, from_address, subject, ns, reply_to_address)
+ except:
+ # log error but keep trying to deliver other notifications,
+ # lest the other notifications (which have already been removed
+ # from the mobx's queue in mongo) be lost
+ log.exception(
+ 'Error sending notifications: [%s] to mbox %s (user %s)',
+ ', '.join([n._id for n in ns]), self._id, self.user_id)
+ elif self.type == 'digest':
+ Notification.send_digest(
+ self.user_id, g.noreply, 'Digest Email',
+ notifications)
+ elif self.type == 'summary':
+ Notification.send_summary(
+ self.user_id, g.noreply, 'Digest Email',
+ notifications)
+
+
+class MailFooter(object):
+ view = jinja2.Environment(
+ loader=jinja2.PackageLoader('allura', 'templates'),
+ auto_reload=asbool(config.get('auto_reload_templates', True)),
+ )
+
+ @classmethod
+ def _render(cls, template, **kw):
+ return cls.view.get_template(template).render(kw)
+
+ @classmethod
+ def standard(cls, notification, allow_email_posting=True, **kw):
+ return cls._render('mail/footer.txt',
+ domain=config['domain'],
+ notification=notification,
+ prefix=config['forgemail.url'],
+ allow_email_posting=allow_email_posting,
+ **kw)
+
+ @classmethod
+ def monitored(cls, toaddr, app_url, setting_url):
+ return cls._render('mail/monitor_email_footer.txt',
+ domain=config['domain'],
+ email=toaddr,
+ app_url=app_url,
+ setting_url=setting_url)
+
+
+class SiteNotification(MappedClass):
+
+ """
+ Storage for site-wide notification.
+ """
+
+ class __mongometa__:
+ session = main_orm_session
+ name = 'site_notification'
+
+ _id = FieldProperty(S.ObjectId)
+ content = FieldProperty(str, if_missing='')
+ active = FieldProperty(bool, if_missing=True)
+ impressions = FieldProperty(
+ int, if_missing=lambda: config.get('site_notification.impressions', 0))
+
+ @classmethod
+ def current(cls):
+ note = cls.query.find().sort('_id', -1).limit(1).first()
+ if note is None or not note.active:
+ return None
+ return note
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/model/oauth.py
----------------------------------------------------------------------
diff --git a/model/oauth.py b/model/oauth.py
new file mode 100644
index 0000000..1e0953b
--- /dev/null
+++ b/model/oauth.py
@@ -0,0 +1,148 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+import oauth2 as oauth
+from pylons import tmpl_context as c, app_globals as g
+
+import pymongo
+from paste.deploy.converters import aslist
+from tg import config
+from ming import schema as S
+from ming.orm import session
+from ming.orm import FieldProperty, RelationProperty, ForeignIdProperty
+from ming.orm.declarative import MappedClass
+
+from allura.lib import helpers as h
+from .session import main_orm_session
+from .types import MarkdownCache
+from .auth import AlluraUserProperty
+
+log = logging.getLogger(__name__)
+
+
+class OAuthToken(MappedClass):
+
+ class __mongometa__:
+ session = main_orm_session
+ name = 'oauth_token'
+ indexes = ['api_key']
+ polymorphic_on = 'type'
+ polymorphic_identity = None
+
+ _id = FieldProperty(S.ObjectId)
+ type = FieldProperty(str)
+ api_key = FieldProperty(str, if_missing=lambda: h.nonce(20))
+ secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce)
+
+ def to_string(self):
+ return oauth.Token(self.api_key, self.secret_key).to_string()
+
+ def as_token(self):
+ return oauth.Token(self.api_key, self.secret_key)
+
+
+class OAuthConsumerToken(OAuthToken):
+
+ class __mongometa__:
+ polymorphic_identity = 'consumer'
+ name = 'oauth_consumer_token'
+ unique_indexes = [('name', 'user_id')]
+
+ type = FieldProperty(str, if_missing='consumer')
+ user_id = AlluraUserProperty(if_missing=lambda: c.user._id)
+ name = FieldProperty(str)
+ description = FieldProperty(str, if_missing='')
+ description_cache = FieldProperty(MarkdownCache)
+
+ user = RelationProperty('User')
+
+ @classmethod
+ def upsert(cls, name):
+ t = cls.query.get(name=name)
+ if t is not None:
+ return t
+ try:
+ t = cls(name=name)
+ session(t).flush(t)
+ except pymongo.errors.DuplicateKeyError:
+ session(t).expunge(t)
+ t = cls.query.get(name=name)
+ return t
+
+ @property
+ def description_html(self):
+ return g.markdown.cached_convert(self, 'description')
+
+ @property
+ def consumer(self):
+ '''OAuth compatible consumer object'''
+ return oauth.Consumer(self.api_key, self.secret_key)
+
+ @classmethod
+ def for_user(cls, user=None):
+ if user is None:
+ user = c.user
+ return cls.query.find(dict(user_id=user._id)).all()
+
+
+class OAuthRequestToken(OAuthToken):
+
+ class __mongometa__:
+ polymorphic_identity = 'request'
+
+ type = FieldProperty(str, if_missing='request')
+ consumer_token_id = ForeignIdProperty('OAuthConsumerToken')
+ user_id = AlluraUserProperty(if_missing=lambda: c.user._id)
+ callback = FieldProperty(str)
+ validation_pin = FieldProperty(str)
+
+ consumer_token = RelationProperty('OAuthConsumerToken')
+
+
+class OAuthAccessToken(OAuthToken):
+
+ class __mongometa__:
+ polymorphic_identity = 'access'
+
+ type = FieldProperty(str, if_missing='access')
+ consumer_token_id = ForeignIdProperty('OAuthConsumerToken')
+ request_token_id = ForeignIdProperty('OAuthToken')
+ user_id = AlluraUserProperty(if_missing=lambda: c.user._id)
+ is_bearer = FieldProperty(bool, if_missing=False)
+
+ user = RelationProperty('User')
+ consumer_token = RelationProperty(
+ 'OAuthConsumerToken', via='consumer_token_id')
+ request_token = RelationProperty('OAuthToken', via='request_token_id')
+
+ @classmethod
+ def for_user(cls, user=None):
+ if user is None:
+ user = c.user
+ return cls.query.find(dict(user_id=user._id, type='access')).all()
+
+ def can_import_forum(self):
+ tokens = aslist(config.get('oauth.can_import_forum', ''), ',')
+ if self.api_key in tokens:
+ return True
+ return False