diff --git a/fas/README b/fas/README
index 66fe76e..834e059 100644
--- a/fas/README
+++ b/fas/README
@@ -27,7 +27,7 @@ Before you can get started, make sure to have the following packages installed
yum install git-core postgresql-plpython postgresql-server postgresql-python \
python-TurboMail TurboGears pygpgme python-sqlalchemy python-genshi \
- python-psycopg2 pytz
+ python-psycopg2 pytz python-babel babel
# Note: on RHEL5 you need postgresql-pl instead of postgresql-plpython
@@ -90,6 +90,9 @@ You'll need to edit dev.cfg and change the following lines::
base_url_filter.base_url = "http://localhost:8080/fas" # Change the port if
# you changed server.socket_port above.
+You may also need to change some of the directories and settings in
+fas/config/app.cfg.
+
You should then be able to start the server and test things out::
./start-fas.py
# browse to http://localhost:8080/fas/
@@ -101,9 +104,9 @@ Make sure you're in the top level directory that start-fas.py and dev.cfg is
in, then run::
tg-admin shell
--------
+--------------------
Enabling Local Users
--------
+--------------------
* THIS IS EXPERIMENTAL *
To allow local users to log in to your system, first enable fas via the
@@ -125,3 +128,17 @@ example:
getent passwd
getent group
+------------
+Localization
+------------
+To generate the POT file (located in the po/ subdirectory), run the
+following from the top level directory:
+
+ pybabel extract -F pybabel.conf -o po/fas.pot .
+To add a language: tg-admin i18n add
+This will create a PO file at po//LC_MESSAGES/fas.po
+
+To update (merge) PO files with the POT file, run:
+
+ tg-admin i18n merge
+
diff --git a/fas/client/fasClient.py b/fas/client/fasClient.py
index 0e3612e..6dba109 100755
--- a/fas/client/fasClient.py
+++ b/fas/client/fasClient.py
@@ -162,12 +162,13 @@ class MakeShellAccounts(BaseClient):
usernames = {}
for person in people:
uid = person['id']
- username = person['username']
- usernames[uid] = username
- file.write("=%i %s:x:%i:\n" % (uid, username, uid))
- file.write("0%i %s:x:%i:\n" % (i, username, uid))
- file.write(".%s %s:x:%i:\n" % (username, username, uid))
- i = i + 1
+ if self.is_valid_user(uid):
+ username = person['username']
+ usernames[uid] = username
+ file.write("=%i %s:x:%i:\n" % (uid, username, uid))
+ file.write("0%i %s:x:%i:\n" % (i, username, uid))
+ file.write(".%s %s:x:%i:\n" % (username, username, uid))
+ i = i + 1
for group in groups:
gid = group['id']
@@ -181,9 +182,9 @@ class MakeShellAccounts(BaseClient):
except KeyError:
''' No users exist in the group '''
pass
- file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, self.memberships))
- file.write("0%i %s:x:%i:%s\n" % (i, name, gid, self.memberships))
- file.write(".%s %s:x:%i:%s\n" % (name, name, gid, self.memberships))
+ file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, memberships))
+ file.write("0%i %s:x:%i:%s\n" % (i, name, gid, memberships))
+ file.write(".%s %s:x:%i:%s\n" % (name, name, gid, memberships))
i = i + 1
file.close()
diff --git a/fas/dev.cfg b/fas/dev.cfg
index e266447..417e454 100644
--- a/fas/dev.cfg
+++ b/fas/dev.cfg
@@ -7,10 +7,9 @@
#mail.server = 'bastion.fedora.phx.redhat.com'
#base_url_filter.base_url = "http://192.168.2.101:8080"
-fas.url = 'http://localhost:8088/fas/'
mail.on = True
-mail.server = 'bastion.fedora.phx.redhat.com'
-mail.testmode = True
+mail.server = 'localhost'
+#mail.testmode = True
mail.debug = False
mail.encoding = 'utf-8'
@@ -53,9 +52,9 @@ autoreload.package="fas"
# unexpected parameter. False by default
tg.strict_parameters = True
-server.webpath='/fas'
+server.webpath='/accounts'
base_url_filter.on=True
-base_url_filter.base_url = "http://localhost:8088/fas"
+base_url_filter.base_url = "https://publictest10.fedoraproject.org/accounts"
# Make the session cookie only return to the host over an SSL link
# Disabled for testing.
diff --git a/fas/fas/__init__.py b/fas/fas/__init__.py
index e69de29..2146c0d 100644
--- a/fas/fas/__init__.py
+++ b/fas/fas/__init__.py
@@ -0,0 +1,27 @@
+class FASError(Exception):
+ '''FAS Error'''
+ pass
+
+class ApplyError(FASError):
+ '''Raised when a user could not apply to a group'''
+ pass
+
+class ApproveError(FASError):
+ '''Raised when a user could not be approved in a group'''
+ pass
+
+class SponsorError(FASError):
+ '''Raised when a user could not be sponsored in a group'''
+ pass
+
+class UpgradeError(FASError):
+ '''Raised when a user could not be upgraded in a group'''
+ pass
+
+class DowngradeError(FASError):
+ '''Raised when a user could not be downgraded in a group'''
+ pass
+
+class RemoveError(FASError):
+ '''Raised when a user could not be removed from a group'''
+ pass
diff --git a/fas/fas/auth.py b/fas/fas/auth.py
index c3484be..faab6d2 100644
--- a/fas/fas/auth.py
+++ b/fas/fas/auth.py
@@ -65,14 +65,7 @@ def isApproved(person, group):
'''
Returns True if the user is an approved member of a group
'''
- try:
- role = PersonRoles.query.filter_by(group=group, member=person).one()
- except IndexError:
- ''' Not in the group '''
- return False
- except InvalidRequestError:
- return False
- if role.role_status == 'approved':
+ if group in person.approved_memberships:
return True
else:
return False
@@ -165,7 +158,7 @@ def canApplyGroup(person, group, applicant):
pass
else:
print "GOT HERE, prereq: %s" % prerequisite
- turbogears.flash(_('%s membership required before application to this group is allowed' % prerequisite.name))
+ turbogears.flash(_('%s membership required before application to this group is allowed') % prerequisite.name)
return False
# A user can apply themselves, and FAS admins can apply other people.
@@ -173,7 +166,7 @@ def canApplyGroup(person, group, applicant):
canAdminGroup(person, group):
return True
else:
- turbogears.flash(_('%s membership required before application to this group is allowed' % prerequisite.name))
+ turbogears.flash(_('%s membership required before application to this group is allowed') % prerequisite.name)
return False
def canSponsorUser(person, group, target):
@@ -208,18 +201,16 @@ def canUpgradeUser(person, group, target):
'''
Returns True if the user can upgrade target in the group
'''
- if isApproved(person, group):
- # Group admins can upgrade anybody.
- # The controller should handle the case where the target
- # is already a group admin.
- if canAdminGroup(person, group):
- return True
- # Sponsors can only upgrade non-sponsors (i.e. normal users)
- elif canSponsorGroup(person, group) and \
- not canSponsorGroup(target, group):
- return True
- else:
- return False
+ # Group admins can upgrade anybody.
+ # The controller should handle the case where the target
+ # is already a group admin.
+ if canAdminGroup(person, group):
+ return True
+ # Sponsors can only upgrade non-sponsors (i.e. normal users)
+ # TODO: Don't assume that canSponsorGroup means that the user is a sponsor
+ elif canSponsorGroup(person, group) and \
+ not canSponsorGroup(target, group):
+ return True
else:
return False
diff --git a/fas/fas/cla.py b/fas/fas/cla.py
index 3b83756..e7abb01 100644
--- a/fas/fas/cla.py
+++ b/fas/fas/cla.py
@@ -53,14 +53,17 @@ class CLA(controllers.Controller):
turbogears.redirect('/user/edit/%s' % username)
if type == 'click':
- if signedCLAPrivs(person):
- turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.'))
- turbogears.redirect('/cla/')
- return dict()
- if clickedCLAPrivs(person):
- turbogears.flash(_('You have already completed the Click-through CLA.'))
- turbogears.redirect('/cla/')
- return dict()
+ # Disable click-through CLA for now
+ #if signedCLAPrivs(person):
+ # turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.'))
+ # turbogears.redirect('/cla/')
+ # return dict()
+ #if clickedCLAPrivs(person):
+ # turbogears.flash(_('You have already completed the Click-through CLA.'))
+ # turbogears.redirect('/cla/')
+ # return dict()
+ turbogears.redirect('/cla/')
+ return dict()
elif type == 'sign':
if signedCLAPrivs(person):
turbogears.flash(_('You have already signed the CLA.'))
@@ -99,12 +102,18 @@ class CLA(controllers.Controller):
data = StringIO.StringIO(signature.file.read())
plaintext = StringIO.StringIO()
verified = False
- try:
- subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', person.gpg_keyid])
- except subprocess.CalledProcessError:
+ keyid = re.sub('\s', '', person.gpg_keyid)
+ ret = subprocess.call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
+ if ret != 0:
turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
turbogears.redirect('/cla/view/sign')
return dict()
+ #try:
+ # subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
+ #except subprocess.CalledProcessError:
+ # turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
+ # turbogears.redirect('/cla/view/sign')
+ # return dict()
else:
try:
sigs = ctx.verify(data, None, plaintext)
@@ -116,7 +125,7 @@ class CLA(controllers.Controller):
if len(sigs):
sig = sigs[0]
# This might still assume a full fingerprint.
- key = ctx.get_key(re.sub('\s', '', person.gpg_keyid))
+ key = ctx.get_key(keyid)
fpr = key.subkeys[0].fpr
if sig.fpr != fpr:
turbogears.flash(_("Your signature's fingerprint did not match the fingerprint registered in FAS."))
@@ -125,7 +134,7 @@ class CLA(controllers.Controller):
emails = [];
for uid in key.uids:
emails.extend([uid.email])
- if person.emails['cla'].email in emails:
+ if person.emails['primary'].email in emails:
verified = True
else:
turbogears.flash(_('Your key did not match your email.'))
@@ -167,13 +176,15 @@ class CLA(controllers.Controller):
person.remove(cilckgroup, person)
except:
pass
+ # TODO: Email legal-cla-archive@fedoraproject.org
turbogears.flash(_("You have successfully signed the CLA. You are now in the '%s' group.") % group.name)
turbogears.redirect('/cla/')
return dict()
@identity.require(turbogears.identity.not_anonymous())
@error_handler(error)
- @expose(template="fas.templates.cla.index")
+ # Don't expose click-through CLA for now.
+ #@expose(template="fas.templates.cla.index")
def click(self, agree):
'''Click-through CLA'''
username = turbogears.identity.current.user_name
diff --git a/fas/fas/client.py b/fas/fas/client.py
deleted file mode 100644
index 36e18d1..0000000
--- a/fas/fas/client.py
+++ /dev/null
@@ -1,228 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2007 Red Hat, Inc. All rights reserved.
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2. This program is distributed in the hope that it
-# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
-# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details. You should have
-# received a copy of the GNU General Public License along with this program;
-# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
-# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
-# incorporated in the source code or documentation are not subject to the GNU
-# General Public License and may only be used or replicated with the express
-# permission of Red Hat, Inc.
-#
-# Red Hat Author(s): Luke Macken
-# Toshio Kuratomi
-#
-
-'''
-python-fedora, python module to interact with Fedora Infrastructure Services
-'''
-
-import Cookie
-import urllib
-import urllib2
-import logging
-import cPickle as pickle
-import re
-import inspect
-import simplejson
-from os import path
-from urlparse import urljoin
-
-import gettext
-t = gettext.translation('python-fedora', '/usr/share/locale', fallback=True)
-_ = t.ugettext
-
-log = logging.getLogger(__name__)
-
-SESSION_FILE = path.join(path.expanduser('~'), '.fedora_session')
-
-class ServerError(Exception):
- pass
-
-class AuthError(ServerError):
- pass
-
-class BaseClient(object):
- '''
- A command-line client to interact with Fedora TurboGears Apps.
- '''
- def __init__(self, baseURL, username=None, password=None, debug=False):
- self.baseURL = baseURL
- self.username = username
- self.password = password
- self._sessionCookie = None
-
- # Setup our logger
- sh = logging.StreamHandler()
- if debug:
- log.setLevel(logging.DEBUG)
- sh.setLevel(logging.DEBUG)
- else:
- log.setLevel(logging.INFO)
- sh.setLevel(logging.INFO)
- format = logging.Formatter("%(message)s")
- sh.setFormatter(format)
- log.addHandler(sh)
-
- self._load_session()
- if username and password:
- self._authenticate(force=True)
-
- def _authenticate(self, force=False):
- '''
- Return an authenticated session cookie.
- '''
- if not force and self._sessionCookie:
- return self._sessionCookie
- if not self.username:
- raise AuthError, _('username must be set')
- if not self.password:
- raise AuthError, _('password must be set')
-
- req = urllib2.Request(urljoin(self.baseURL, 'login?tg_format=json'))
- req.add_header('Cookie', self._sessionCookie.output(attrs=[],
- header='').strip())
- req.add_data(urllib.urlencode({
- 'user_name' : self.username,
- 'password' : self.password,
- 'login' : 'Login'
- }))
-
- try:
- loginPage = urllib2.urlopen(req)
- except urllib2.HTTPError, e:
- if e.msg == 'Forbidden':
- raise AuthError, _('Invalid username/password')
- else:
- raise
-
- loginData = simplejson.load(loginPage)
-
- if 'message' in loginData:
- raise AuthError, _('Unable to login to server: %(message)s') \
- % loginData
-
- self._sessionCookie = Cookie.SimpleCookie()
- try:
- self._sessionCookie.load(loginPage.headers['set-cookie'])
- except KeyError:
- self._sessionCookie = None
- raise AuthError, _('Unable to login to the server. Server did' \
- ' not send back a cookie')
- self._save_session()
-
- return self._sessionCookie
- session = property(_authenticate)
-
- def _save_session(self):
- '''
- Store our pickled session cookie.
-
- This method loads our existing session file and modified our
- current user's cookie. This allows us to retain cookies for
- multiple users.
- '''
- save = {}
- if path.isfile(SESSION_FILE):
- sessionFile = file(SESSION_FILE, 'r')
- try:
- save = pickle.load(sessionFile)
- except:
- pass
- sessionFile.close()
- save[self.username] = self._sessionCookie
- sessionFile = file(SESSION_FILE, 'w')
- pickle.dump(save, sessionFile)
- sessionFile.close()
-
- def _load_session(self):
- '''
- Load a stored session cookie.
- '''
- if path.isfile(SESSION_FILE):
- sessionFile = file(SESSION_FILE, 'r')
- try:
- savedSession = pickle.load(sessionFile)
- self._sessionCookie = savedSession[self.username]
- log.debug(_('Loaded session %(cookie)s') % \
- {'cookie': self._sessionCookie})
- except EOFError:
- log.error(_('Unable to load session from %(file)s') % \
- {'file': SESSION_FILE})
- except KeyError:
- log.debug(_('Session is for a different user'))
- sessionFile.close()
-
- def send_request(self, method, auth=False, input=None):
- '''
- Send a request to the server. The given method is called with any
- keyword parameters in **kw. If auth is True, then the request is
- made with an authenticated session cookie.
- '''
- url = urljoin(self.baseURL, method + '?tg_format=json')
-
- response = None # the JSON that we get back from the server
- data = None # decoded JSON via simplejson.load()
-
- log.debug(_('Creating request %(url)s') % {'url': url})
- req = urllib2.Request(url)
- if input:
- req.add_data(urllib.urlencode(input))
-
- if auth:
- req.add_header('Cookie', self.session.output(attrs=[],
- header='').strip())
- elif self._sessionCookie:
- # If the cookie exists, send it so that visit tracking works.
- req.add_header('Cookie', self._sessionCookie.output(attrs=[],
- header='').strip())
- try:
- response = urllib2.urlopen(req)
- except urllib2.HTTPError, e:
- if e.msg == 'Forbidden':
- if (inspect.currentframe().f_back.f_code !=
- inspect.currentframe().f_code):
- self._authenticate(force=True)
- data = self.send_request(method, auth, input)
- else:
- # We actually shouldn't ever reach here. Unless something
- # goes drastically wrong _authenticate should raise an
- # AuthError
- raise AuthError, _('Unable to log into server: %(error)s') \
- % {'error': str(e)}
- log.error(e)
- raise ServerError, str(e)
-
- # In case the server returned a new session cookie to us
- try:
- self._sessionCookie.load(response.headers['set-cookie'])
- except KeyError:
- pass
-
- try:
- data = simplejson.load(response)
- except Exception, e:
- regex = re.compile('(.*)')
- match = regex.search(response)
- if match and len(match.groups()):
- return dict(tg_flash=match.groups()[0])
- else:
- raise ServerError, e.message
-
- if 'logging_in' in data:
- if (inspect.currentframe().f_back.f_code !=
- inspect.currentframe().f_code):
- self._authenticate(force=True)
- data = self.send_request(method, auth, input)
- else:
- # We actually shouldn't ever reach here. Unless something goes
- # drastically wrong _authenticate should raise an AuthError
- raise AuthError, _('Unable to log into server: %(message)s') \
- % data
- return data
diff --git a/fas/fas/config/app.cfg b/fas/fas/config/app.cfg
index 7bab914..0aafe08 100644
--- a/fas/fas/config/app.cfg
+++ b/fas/fas/config/app.cfg
@@ -6,6 +6,9 @@
# The commented out values below are the defaults
+# Database values
+sqlalchemy.convert_unicode=True
+
admingroup = 'accounts'
# VIEW
@@ -151,9 +154,11 @@ identity.saprovider.model.group="fas.model.Groups"
# identity.saprovider.encryption_algorithm=None
accounts_mail = "accounts@fedoraproject.org"
+#email_host = "fedoraproject.org"
+email_host = "publictest10.fedoraproject.org"
gpgexec = "/usr/bin/gpg"
-gpghome = "/home/ricky/work/fedora/fedora-infrastructure/fas/gnupg"
+gpghome = "/srv/fedora-infrastructure/fas/gnupg"
gpg_fingerprint = "C199 1E25 D00A D200 2D2E 54D1 BF7F 1647 C54E 8410"
gpg_passphrase = "m00!s@ysth3c0w"
gpg_keyserver = "hkp://subkeys.pgp.net"
@@ -168,7 +173,7 @@ openidstore = "/var/tmp/fas/openid"
openssl_digest = "md5"
openssl_expire = 31536000 # 60*60*24*365 = 1 year
-openssl_ca_file = "/home/ricky/work/fedora/fedora-infrastructure/fas/ssl/ca-Upload"
+openssl_ca_file = "/srv/fedora-infrastructure/fas/ssl/ca-Upload"
openssl_c = "US"
openssl_st = "North Carolina"
openssl_l = "Raleigh"
diff --git a/fas/fas/controllers.py b/fas/fas/controllers.py
index 8ae178e..674edb8 100644
--- a/fas/fas/controllers.py
+++ b/fas/fas/controllers.py
@@ -13,6 +13,7 @@ from fas.group import Group
from fas.cla import CLA
from fas.json_request import JsonRequest
from fas.help import Help
+from fas.auth import *
#from fas.openid_fas import OpenID
import os
@@ -28,9 +29,14 @@ turbogears.view.variable_providers.append(add_custom_stdvars)
def get_locale(locale=None):
if locale:
return locale
- if turbogears.identity.current.user_name:
+ username = None
+ try:
+ username = turbogears.identity.current.user_name
+ except AttributeError:
+ pass
+ if username:
person = People.by_username(turbogears.identity.current.user_name)
- return person.locale
+ return person.locale or 'C'
else:
return turbogears.i18n.utils._get_locale()
@@ -58,13 +64,24 @@ class Root(controllers.RootController):
@expose(template="fas.templates.welcome", allow_json=True)
def index(self):
if turbogears.identity.not_anonymous():
+ if 'tg_format' in request.params \
+ and request.params['tg_format'] == 'json':
+ # redirects don't work with JSON calls. This is a bit of a
+ # hack until we can figure out something better.
+ return dict()
turbogears.redirect('/home')
return dict(now=time.ctime())
- @expose(template="fas.templates.home")
+ @expose(template="fas.templates.home", allow_json=True)
@identity.require(identity.not_anonymous())
def home(self):
- return dict()
+ user_name = turbogears.identity.current.user_name
+ person = People.by_username(user_name)
+ cla = None
+ if signedCLAPrivs(person):
+ cla = 'signed'
+
+ return dict(person=person, cla=cla)
@expose(template="fas.templates.about")
def about(self):
@@ -93,7 +110,7 @@ class Root(controllers.RootController):
# is better.
return dict(user = identity.current.user)
if not forward_url:
- forward_url = config.get('base_url_filter.base_url') + '/'
+ forward_url = '/'
raise redirect(forward_url)
forward_url=None
@@ -107,7 +124,7 @@ class Root(controllers.RootController):
"this resource.")
else:
msg=_("Please log in.")
- forward_url= request.headers.get("Referer", "/")
+ forward_url= '/'
### FIXME: Is it okay to get rid of this?
#cherrypy.response.status=403
@@ -125,7 +142,7 @@ class Root(controllers.RootController):
# redirect to a page. Returning the logged in identity
# is better.
return dict(status=True)
- raise redirect(request.headers.get("Referer", "/"))
+ raise redirect('/')
@expose()
def language(self, locale):
diff --git a/fas/fas/group.py b/fas/fas/group.py
index 8fc685b..f896c77 100644
--- a/fas/fas/group.py
+++ b/fas/fas/group.py
@@ -4,8 +4,8 @@ from turbogears.database import session
import cherrypy
+import fas
from fas.auth import *
-
from fas.user import KnownUser
import re
@@ -37,7 +37,7 @@ class GroupCreate(validators.Schema):
name = validators.All(
UnknownGroup,
validators.String(max=32, min=3),
- validators.Regex(regex='^[a-z][a-z0-9]+$'),
+ validators.Regex(regex='^[a-z0-9\-]+$'),
)
display_name = validators.NotEmpty
owner = KnownUser
@@ -165,8 +165,8 @@ class Group(controllers.Controller):
group.display_name = display_name
group.owner_id = person_owner.id
group.group_type = group_type
- group.needs_sponsor = needs_sponsor
- group.user_can_remove = user_can_remove
+ group.needs_sponsor = bool(needs_sponsor)
+ group.user_can_remove = bool(user_can_remove)
if prerequisite:
prerequisite = Groups.by_name(prerequisite)
group.prerequisite = prerequisite
@@ -224,8 +224,8 @@ class Group(controllers.Controller):
group.display_name = display_name
group.owner = owner
group.group_type = group_type
- group.needs_sponsor = needs_sponsor
- group.user_can_remove = user_can_remove
+ group.needs_sponsor = bool(needs_sponsor)
+ group.user_can_remove = bool(user_can_remove)
if prerequisite:
prerequisite = Groups.by_name(prerequisite)
group.prerequisite = prerequisite
@@ -279,15 +279,16 @@ class Group(controllers.Controller):
else:
try:
target.apply(group, person)
- except: # TODO: More specific exception here.
- turbogears.flash(_('%(user)s has already applied to %(group)s!') % \
- {'user': target.username, 'group': group.name})
+ except fas.ApplyError, e:
+ turbogears.flash(_('%(user)s could not apply to %(group)s: %(error)s') % \
+ {'user': target.username, 'group': group.name, 'error': e})
+ turbogears.redirect('/group/view/%s' % group.name)
else:
import turbomail
-
# TODO: How do we handle gettext calls for these kinds of emails?
# TODO: CC to right place, put a bit more thought into how to most elegantly do this
- message = turbomail.Message(config.get('accounts_mail'), '%s-sponsors@fedoraproject.org' % group.name, \
+ # TODO: Maybe that @fedoraproject.org (and even -sponsors) should be configurable somewhere?
+ message = turbomail.Message(config.get('accounts_mail'), '%(group)s-sponsors@%(host)s' % {'group': group.name, 'host': config.get('email_host')}, \
"Fedora '%(group)s' sponsor needed for %(user)s" % {'user': target.username, 'group': group.name})
url = config.get('base_url_filter.base_url') + turbogears.url('/group/edit/%s' % groupname)
@@ -321,8 +322,9 @@ Please go to %(url)s to take action.
else:
try:
target.sponsor(group, person)
- except:
- turbogears.flash(_("'%s' could not be sponsored!") % target.username)
+ except fas.SponsorError, e:
+ turbogears.flash(_("%(user)s could not be sponsored in %(group)s: %(error)s") % \
+ {'user': target.username, 'group': group.name, 'error': e})
turbogears.redirect('/group/view/%s' % group.name)
else:
import turbomail
@@ -335,7 +337,7 @@ propagate into the e-mail aliases and CVS repository within an hour.
%(joinmsg)s
''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg}
turbomail.enqueue(message)
- turbogears.flash(_("'%s' has been sponsored!") % person.human_name)
+ turbogears.flash(_("'%s' has been sponsored!") % target.human_name)
turbogears.redirect('/group/view/%s' % group.name)
return dict()
@@ -358,9 +360,9 @@ propagate into the e-mail aliases and CVS repository within an hour.
else:
try:
target.remove(group, target)
- except KeyError:
- turbogears.flash(_('%(name)s could not be removed from %(group)s!') % \
- {'name': target.username, 'group': group.name})
+ except fas.RemoveError, e:
+ turbogears.flash(_("%(user)s could not be removed from %(group)s: %(error)s") % \
+ {'user': target.username, 'group': group.name, 'error': e})
turbogears.redirect('/group/view/%s' % group.name)
else:
import turbomail
@@ -372,7 +374,7 @@ immediately for new operations, and should propagate into the e-mail
aliases within an hour.
''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email}
turbomail.enqueue(message)
- turbogears.flash(_('%(name)s has been removed from %(group)s!') % \
+ turbogears.flash(_('%(name)s has been removed from %(group)s') % \
{'name': target.username, 'group': group.name})
turbogears.redirect('/group/view/%s' % group.name)
return dict()
@@ -395,11 +397,9 @@ aliases within an hour.
else:
try:
target.upgrade(group, person)
- except TypeError, e:
- turbogears.flash(e)
- turbogears.redirect('/group/view/%s' % group.name)
- except:
- turbogears.flash(_('%(name)s could not be upgraded!') % {'name' : target.username})
+ except fas.UpgradeError, e:
+ turbogears.flash(_('%(name)s could not be upgraded in %(group)s: %(error)s') % \
+ {'name': target.username, 'group': group.name, 'error': e})
turbogears.redirect('/group/view/%s' % group.name)
else:
import turbomail
@@ -436,8 +436,9 @@ into the e-mail aliases within an hour.
else:
try:
target.downgrade(group, person)
- except:
- turbogears.flash(_('%(username)s could not be downgraded!') % {'username': target.username})
+ except fas.DowngradeError, e:
+ turbogears.flash(_('%(name)s could not be downgraded in %(group)s: %(error)s') % \
+ {'name': target.username, 'group': group.name, 'error': e})
turbogears.redirect('/group/view/%s' % group.name)
else:
import turbomail
@@ -469,7 +470,7 @@ into the e-mail aliases within an hour.
turbogears.redirect('/group/list')
return dict()
else:
- return dict(groups=groups)
+ return dict(group=group)
@identity.require(identity.not_anonymous())
@validate(validators=GroupInvite())
diff --git a/fas/fas/help.py b/fas/fas/help.py
index 23dd5d6..391eacd 100644
--- a/fas/fas/help.py
+++ b/fas/fas/help.py
@@ -5,17 +5,25 @@ from turbogears.database import session
from fas.auth import *
class Help(controllers.Controller):
- help = { 'none' : ['Error', 'We could not find that help item
'],
- 'user_ircnick' : ['IRC Nick (Optional)', 'IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to
'],
- 'user_primary_email' : ['Primary Email (Required)', 'This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded
'],
- 'user_human_name' : ['Full Name (Required)', 'Your Human Name or "real life" name
'],
- 'user_gpg_keyid' : ['GPG Key', 'Only required for users signing the CLA. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the CLAHowTo for more information
'],
- 'user_telephone' : ['Telephone', 'Only required for users signing the CLA. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our Privacy Policy
'],
- 'user_postal_address': ['Postal Address', 'Only required for users signing the CLA. This should be a mailing address where you can be contacted. See our Privacy Policy about any concerns.
'],
- 'user_timezone': ['Timezone (Optional)', 'Please specify the time zone you are in.
'],
- 'user_comments': ['Comments (Optional)', 'Misc comments about yourself.
'],
- 'user_account_status': ['Account Status', 'Shows account status, possible values include
'],
- 'user_cla' : ['CLA', 'In order to become a full Fedora contributor you must sign a Contributor License Agreement. This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the CLAHowTo.
'],
+ help = { 'none' : ['Error', 'We could not find that help item
'],
+ 'user_ircnick' : ['IRC Nick (Optional)', 'IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to
'],
+ 'user_primary_email' : ['Primary Email (Required)', 'This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded
'],
+ 'user_human_name' : ['Full Name (Required)', 'Your Human Name or "real life" name
'],
+ 'user_gpg_keyid' : ['GPG Key', 'Only required for users signing the CLA. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the CLAHowTo for more information
'],
+ 'user_telephone' : ['Telephone', 'Only required for users signing the CLA. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our Privacy Policy
'],
+ 'user_postal_address': ['Postal Address', 'Only required for users signing the CLA. This should be a mailing address where you can be contacted. See our Privacy Policy about any concerns.
'],
+ 'user_timezone': ['Timezone (Optional)', 'Please specify the time zone you are in.
'],
+ 'user_comments': ['Comments (Optional)', 'Misc comments about yourself.
'],
+ 'user_account_status': ['Account Status', 'Shows account status, possible values include
'],
+ 'user_cla' : ['CLA', 'In order to become a full Fedora contributor you must sign a Contributor License Agreement. This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the CLAHowTo.
'],
+ 'user_locale': ['Locale', 'For non-english speaking peoples this allows individuals to select which locale they are in.
'],
+
+ 'group_apply': ['Apply', 'Applying for a group is like applying for a job and it can certainly take a while to get in. Many groups have their own rules about how to actually get approved or sponsored. For more information on how the account system works see the about page.
'],
+ 'group_remove': ['Remove', '''Removing a person from a group will cause that user to no longer be in the group. They will need to re-apply to get in. Admins can remove anyone, Sponsors can remove users, users can't remove anyone.
'''],
+ 'group_upgrade': ['Upgrade', '''Upgrade a persons status in this group.
- from user -> to sponsor
- From sponsor -> administrator
- administrators cannot be upgraded beyond administrator
'''],
+ 'group_downgrade': ['Downgrade', '''Downgrade a persons status in the group.
- from administrator -> to sponsor
- From sponsor -> user
- users cannot be downgraded below user, you may want to remove them
'''],
+ 'group_approve': ['Approve', '''A sponsor or administrator can approve users to be in a group. Once the user has applied for the group, go to the group's page and click approve to approve the user.
'''],
+ 'group_sponsor': ['Sponsor', '''A sponsor or administrator can sponsor users to be in a gruop. Once the user has applied for the group, go to the group's page and click approve to sponsor the user. Sponsorship of a user implies that you are approving a user and may mentor and answer their questions as they come up.
'''],
}
def __init__(self):
diff --git a/fas/fas/json.py b/fas/fas/json.py
deleted file mode 100644
index 4498c1c..0000000
--- a/fas/fas/json.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2007-2008 Red Hat, Inc. All rights reserved.
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2. This program is distributed in the hope that it
-# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
-# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details. You should have
-# received a copy of the GNU General Public License along with this program;
-# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
-# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
-# incorporated in the source code or documentation are not subject to the GNU
-# General Public License and may only be used or replicated with the express
-# permission of Red Hat, Inc.
-#
-# Red Hat Author(s): Toshio Kuratomi
-#
-
-'''
-JSON Helper functions. Most JSON code directly related to classes is
-implemented via the __json__() methods in model.py. These methods define
-methods of transforming a class into json for a few common types.
-'''
-# A JSON-based API(view) for your app.
-# Most rules would look like:
-# @jsonify.when("isinstance(obj, YourClass)")
-# def jsonify_yourclass(obj):
-# return [obj.val1, obj.val2]
-# @jsonify can convert your objects to following types:
-# lists, dicts, numbers and strings
-
-import sqlalchemy
-from turbojson.jsonify import jsonify
-
-class SABase(object):
- '''Base class for SQLAlchemy mapped objects.
-
- This base class makes sure we have a __json__() method on each SQLAlchemy
- mapped object that knows how to::
-
- 1) return json for the object.
- 2) Can selectively add tables pulled in from the table to the data we're
- returning.
- '''
- # pylint: disable-msg=R0903
- def __json__(self):
- '''Transform any SA mapped class into json.
-
- This method takes an SA mapped class and turns the "normal" python
- attributes into json. The properties (from properties in the mapper)
- are also included if they have an entry in jsonProps. You make
- use of this by setting jsonProps in the controller.
-
- Example controller::
- john = model.Person.get_by(name='John')
- # Person has a property, addresses, linking it to an Address class.
- # Address has a property, phone_nums, linking it to a Phone class.
- john.jsonProps = {'Person': ['addresses'],
- 'Address': ['phone_nums']}
- return dict(person=john)
-
- jsonProps is a dict that maps class names to lists of properties you
- want to output. This allows you to selectively pick properties you
- are interested in for one class but not another. You are responsible
- for avoiding loops. ie: *don't* do this::
- john.jsonProps = {'Person': ['addresses'], 'Address': ['people']}
- '''
- props = {}
- # pylint: disable-msg=E1101
- if 'jsonProps' in self.__dict__ \
- and self.jsonProps.has_key(self.__class__.__name__):
- propList = self.jsonProps[self.__class__.__name__]
- else:
- propList = {}
- # pylint: enable-msg=E1101
-
- # Load all the columns from the table
- for column in sqlalchemy.orm.object_mapper(self).iterate_properties:
- if isinstance(column, sqlalchemy.orm.properties.ColumnProperty):
- props[column.key] = getattr(self, column.key)
-
- # Load things that are explicitly listed
- for field in propList:
- props[field] = getattr(self, field)
- try:
- # pylint: disable-msg=E1101
- props[field].jsonProps = self.jsonProps
- except AttributeError: # pylint: disable-msg=W0704
- # Certain types of objects are terminal and won't allow setting
- # jsonProps
- pass
- return props
-
-@jsonify.when("isinstance(obj, sqlalchemy.orm.query.Query)")
-def jsonify_sa_select_results(obj):
- '''Transform selectresults into lists.
-
- The one special thing is that we bind the special jsonProps into each
- descendent. This allows us to specify a jsonProps on the toplevel
- query result and it will pass to all of its children.
- '''
- if 'jsonProps' in obj.__dict__:
- for element in obj:
- element.jsonProps = obj.jsonProps
- return list(obj)
-
-@jsonify.when("isinstance(obj, sqlalchemy.orm.attributes.InstrumentedAttribute) or isinstance(obj, sqlalchemy.ext.associationproxy._AssociationList)")
-def jsonify_salist(obj):
- '''Transform SQLAlchemy InstrumentedLists into json.
-
- The one special thing is that we bind the special jsonProps into each
- descendent. This allows us to specify a jsonProps on the toplevel
- query result and it will pass to all of its children.
- '''
- if 'jsonProps' in obj.__dict__:
- for element in obj:
- element.jsonProps = obj.jsonProps
- return [jsonify(element) for element in obj]
diff --git a/fas/fas/jsonfasprovider.py b/fas/fas/jsonfasprovider.py
deleted file mode 100644
index 73c09b6..0000000
--- a/fas/fas/jsonfasprovider.py
+++ /dev/null
@@ -1,234 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2007-2008 Red Hat, Inc. All rights reserved.
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2. This program is distributed in the hope that it
-# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
-# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details. You should have
-# received a copy of the GNU General Public License along with this program;
-# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
-# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
-# incorporated in the source code or documentation are not subject to the GNU
-# General Public License and may only be used or replicated with the express
-# permission of Red Hat, Inc.
-#
-# Red Hat Author(s): Toshio Kuratomi
-#
-
-'''
-This plugin provides integration with the Fedora Account
-System using JSON calls.
-'''
-
-import Cookie
-
-from cherrypy import request
-from sqlalchemy.orm import class_mapper
-from turbogears import config, identity
-from turbogears.identity.saprovider import SqlAlchemyIdentity, \
- SqlAlchemyIdentityProvider
-from turbogears.database import session
-from turbogears.util import load_class
-
-# Once this works, propogate the changes back to python-fedora and import as
-# from fedora.tg.client import BaseClient
-from client import BaseClient
-
-import gettext
-t = gettext.translation('python-fedora', '/usr/share/locale', fallback=True)
-_ = t.ugettext
-
-import crypt
-
-import logging
-log = logging.getLogger('turbogears.identity.safasprovider')
-
-try:
- set, frozenset
-except NameError:
- from sets import Set as set, ImmutableSet as frozenset
-
-class JsonFasIdentity(BaseClient):
- '''Associate an identity with a person in the auth system.
- '''
- cookieName = config.get('visit.cookie.name', 'tg-visit')
- fasURL = config.get('fas.url', 'https://admin.fedoraproject.org/admin/fas/')
-
- def __init__(self, visit_key, user=None, username=None, password=None,
- debug=False):
- super(JsonFasIdentity, self).__init__(self.fasURL, debug=debug)
- if user:
- self._user = user
- self._groups = frozenset(
- [g['name'] for g in data['person']['approved_memberships']]
- )
- self.visit_key = visit_key
- # It's allowed to use a null value for a visit_key if we know we're
- # generating an anonymous user. The json interface doesn't handle
- # that, though, and there's no reason for us to make it.
- if not visit_key:
- return
-
- # Set the cookie to the user's tg_visit key before requesting
- # authentication. That way we link the two together.
- self._sessionCookie = Cookie.SimpleCookie()
- self._sessionCookie[self.cookieName] = self.visit_key
- self.username = username
- self.password = password
- if username and password:
- self._authenticate(force=True)
-
- def _authenticate(self, force=False):
- '''Override BaseClient so we can keep visit_key in sync.
- '''
- super(JsonFasIdentity, self)._authenticate(force)
- if self._sessionCookie[self.cookieName].value != self.visit_key:
- # When the visit_key changes (because the old key had expired or
- # been deleted from the db) change the visit_key in our variables
- # and the session cookie to be sent back to the client.
- self.visit_key = self._sessionCookie[self.cookieName].value
- cookies = request.simple_cookie
- cookies[self.cookieName] = self.visit_key
- return self._sessionCookie
- session = property(_authenticate)
-
- def _get_user(self):
- '''Retrieve information about the user from cache or network.'''
- try:
- return self._user
- except AttributeError:
- # User hasn't already been set
- pass
- # Attempt to load the user. After this code executes, there *WILL* be
- # a _user attribute, even if the value is None.
- # Query the account system URL for our given user's sessionCookie
- # FAS returns user and group listing
- data = self.send_request('user/view', auth=True)
- if not data['person']:
- self._user = None
- return None
- self._user = data['person']
- self._groups = frozenset(
- [g['name'] for g in data['person']['approved_memberships']]
- )
- return self._user
- user = property(_get_user)
-
- def _get_user_name(self):
- if not self.user:
- return None
- return self.user['username']
- user_name = property(_get_user_name)
-
- def _get_groups(self):
- try:
- return self._groups
- except AttributeError:
- # User and groups haven't been returned. Since the json call
- # returns both user and groups, this is set at user creation time.
- self._groups = frozenset()
- return self._groups
- groups = property(_get_groups)
-
- def logout(self):
- '''
- Remove the link between this identity and the visit.
- '''
- if not self.visit_key:
- return
- # Call Account System Server logout method
- self.send_request('logout', auth=True)
-
-class JsonFasIdentityProvider(object):
- '''
- IdentityProvider that authenticates users against the fedora account system
- '''
- def __init__(self):
- # Default encryption algorithm is to use plain text passwords
- algorithm = config.get("identity.saprovider.encryption_algorithm", None)
- self.encrypt_password = lambda pw: \
- identity._encrypt_password(algorithm, pw)
-
- def create_provider_model(self):
- '''
- Create the database tables if they don't already exist.
- '''
- # No database tables to create because the db is behind the FAS2
- # server
- pass
-
- def validate_identity(self, user_name, password, visit_key):
- '''
- Look up the identity represented by user_name and determine whether the
- password is correct.
-
- Must return either None if the credentials weren't valid or an object
- with the following properties:
- user_name: original user name
- user: a provider dependant object (TG_User or similar)
- groups: a set of group IDs
- permissions: a set of permission IDs
- '''
- try:
- user = JsonFasIdentity(visit_key, username=user_name,
- password=password)
- except AuthError, e:
- log.warning('Error logging in %(user)s: %(error)s' % {
- 'user': username, 'error': e})
- return None
-
- return JsonFasIdentity(visit_key, user)
-
- def validate_password(self, user, user_name, password):
- '''
- Check the supplied user_name and password against existing credentials.
- Note: user_name is not used here, but is required by external
- password validation schemes that might override this method.
- If you use SqlAlchemyIdentityProvider, but want to check the passwords
- against an external source (i.e. PAM, LDAP, Windows domain, etc),
- subclass SqlAlchemyIdentityProvider, and override this method.
-
- Arguments:
- :user: User information. Not used.
- :user_name: Given username.
- :password: Given, plaintext password.
-
- Returns: True if the password matches the username. Otherwise False.
- Can return False for problems within the Account System as well.
- '''
-
- return user.password == crypt.crypt(password, user.password)
-
- def load_identity(self, visit_key):
- '''Lookup the principal represented by visit_key.
-
- Arguments:
- :visit_key: The session key for whom we're looking up an identity.
-
- Must return an object with the following properties:
- user_name: original user name
- user: a provider dependant object (TG_User or similar)
- groups: a set of group IDs
- permissions: a set of permission IDs
- '''
- return JsonFasIdentity(visit_key)
-
- def anonymous_identity(self):
- '''
- Must return an object with the following properties:
- user_name: original user name
- user: a provider dependant object (TG_User or similar)
- groups: a set of group IDs
- permissions: a set of permission IDs
- '''
-
- return JsonFasIdentity(None)
-
- def authenticated_identity(self, user):
- '''
- Constructs Identity object for user that has no associated visit_key.
- '''
- return JsonFasIdentity(None, user)
diff --git a/fas/fas/jsonfasvisit.py b/fas/fas/jsonfasvisit.py
deleted file mode 100644
index 0bdd38a..0000000
--- a/fas/fas/jsonfasvisit.py
+++ /dev/null
@@ -1,82 +0,0 @@
-'''
-This plugin provides integration with the Fedora Account System using JSON
-calls to the account system server.
-'''
-
-import Cookie
-
-from datetime import datetime
-
-from sqlalchemy import *
-from sqlalchemy.orm import class_mapper
-
-from turbogears import config
-from turbogears.visit.api import BaseVisitManager, Visit
-from turbogears.database import get_engine, metadata, session, mapper
-from turbogears.util import load_class
-
-# Once this works, propogate the changes back to python-fedora and import as
-# from fedora.tg.client import BaseClient
-from client import BaseClient
-
-import gettext
-t = gettext.translation('python-fedora', '/usr/share/locale', fallback=True)
-_ = t.ugettext
-
-import logging
-log = logging.getLogger("turbogears.identity.savisit")
-
-class JsonFasVisitManager(BaseClient):
- '''
- This proxies visit requests to the Account System Server running remotely.
-
- We don't need to worry about threading and other concerns because our proxy
- doesn't cause any asynchronous calls.
- '''
- fasURL = config.get('fas.url', 'https://admin.fedoraproject.org/admin/fas')
- cookieName = config.get('visit.cookie.name', 'tg-visit')
-
- def __init__(self, timeout, debug=None):
- super(JsonFasVisitManager,self).__init__(self.fasURL, debug=debug)
-
- def create_model(self):
- '''
- Create the Visit table if it doesn't already exist
- '''
- # Not needed as the visit tables reside remotely in the FAS2 database.
- pass
-
- def new_visit_with_key(self, visit_key):
- # Hit any URL in fas2 with the visit_key set. That will call the
- # new_visit method in fas2
- self._sessionCookie = Cookie.SimpleCookie()
- self._sessionCookie[self.cookieName] = visit_key
- data = self.send_request('', auth=True)
- return Visit(self._sessionCookie[self.cookieName].value, True)
-
- def visit_for_key(self, visit_key):
- '''
- Return the visit for this key or None if the visit doesn't exist or has
- expired.
- '''
- # Hit any URL in fas2 with the visit_key set. That will call the
- # new_visit method in fas2
- self._sessionCookie = Cookie.SimpleCookie()
- self._sessionCookie[self.cookieName] = visit_key
- data = self.send_request('', auth=True)
- # Knowing what happens in turbogears/visit/api.py when this is called,
- # we can shortcircuit this step and avoid a round trip to the FAS
- # server.
- # if visit_key != self._sessionCookie[self.cookieName].value:
- # # visit has expired
- # return None
- # # Hitting FAS has already updated the visit.
- # return Visit(visit_key, False)
- if visit_key != self._sessionCookie[self.cookieName].value:
- return Visit(self._sessionCookie[self.cookieName].value, True)
- else:
- return Visit(visit_key, False)
-
- def update_queued_visits(self, queue):
- # Let the visit_manager on the FAS server manage this
- pass
diff --git a/fas/fas/model.py b/fas/fas/model.py
index bd02b6b..97bb28b 100644
--- a/fas/fas/model.py
+++ b/fas/fas/model.py
@@ -42,10 +42,10 @@ from turbogears.database import session
from turbogears import identity
-from fas.json import SABase
+import turbogears
-# Soon we'll use this instead:
-#from fedora.tg.json import SABase
+from fedora.tg.json import SABase
+import fas
# Bind us to the database defined in the config file.
get_engine()
@@ -121,18 +121,21 @@ class People(SABase):
'''
Apply a person to a group
'''
- role = PersonRoles()
- role.role_status = 'unapproved'
- role.role_type = 'user'
- role.member = cls
- role.group = group
+ if group in cls.memberships:
+ raise fas.ApplyError, _('user is already in this group')
+ else:
+ role = PersonRoles()
+ role.role_status = 'unapproved'
+ role.role_type = 'user'
+ role.member = cls
+ role.group = group
def approve(cls, group, requester):
'''
Approve a person in a group - requester for logging purposes
'''
- if group in cls.approved_memberships:
- raise '%s is already approved in %s' % (cls.username, group.name)
+ if group not in cls.unapproved_memberships:
+ raise fas.ApproveError, _('user is not an unapproved member')
else:
role = PersonRoles.query.filter_by(member=cls, group=group).one()
role.role_status = 'approved'
@@ -142,11 +145,11 @@ class People(SABase):
Upgrade a user in a group - requester for logging purposes
'''
if not group in cls.memberships:
- raise '%s not a member of %s' % (group.name, cls.memberships)
+ raise fas.UpgradeError, _('user is not a member')
else:
role = PersonRoles.query.filter_by(member=cls, group=group).one()
if role.role_type == 'administrator':
- raise '%s is already an admin in %s' % (cls.username, group.name)
+ raise fas.UpgradeError, _('administrators cannot be upgraded any further')
elif role.role_type == 'sponsor':
role.role_type = 'administrator'
elif role.role_type == 'user':
@@ -157,11 +160,11 @@ class People(SABase):
Downgrade a user in a group - requester for logging purposes
'''
if not group in cls.memberships:
- raise '%s not a member of %s' % (group.name, cls.memberships)
+ raise fas.DowngradeError, _('user is not a member')
else:
role = PersonRoles.query.filter_by(member=cls, group=group).one()
if role.role_type == 'user':
- raise '%s is already a user in %s, did you mean to remove?' % (cls.username, group.name)
+ raise fas.DowngradeError, _('users cannot be downgraded any further')
elif role.role_type == 'sponsor':
role.role_type = 'user'
elif role.role_type == 'administrator':
@@ -169,20 +172,19 @@ class People(SABase):
def sponsor(cls, group, requester):
# If we want to do logging, this might be the place.
- if not group in cls.memberships:
- raise '%s not a member of %s' % (group.name, cls.memberships)
+ if not group in cls.unapproved_memberships:
+ raise fas.SponsorError, _('user is not an unapproved member')
role = PersonRoles.query.filter_by(member=cls, group=group).one()
role.role_status = 'approved'
role.sponsor_id = requester.id
role.approval = datetime.now(pytz.utc)
def remove(cls, group, requester):
- role = PersonRoles.query.filter_by(member=cls, group=group).one()
- try:
+ if not group in cls.memberships:
+ raise fas.RemoveError, _('user is not a member')
+ else:
+ role = PersonRoles.query.filter_by(member=cls, group=group).one()
session.delete(role)
- except TypeError:
- pass
- # Handle somehow.
def __repr__(cls):
return "User(%s,%s)" % (cls.username, cls.human_name)
diff --git a/fas/fas/openid_fas.py b/fas/fas/openid_fas.py
index e3d6635..f9ca8c7 100644
--- a/fas/fas/openid_fas.py
+++ b/fas/fas/openid_fas.py
@@ -106,7 +106,7 @@ class OpenID(controllers.Controller):
@validate(validators=UserID())
def id(self, username):
'''The "real" OpenID URL'''
- person = Person.by_username(username)
+ person = People.by_username(username)
server = config.get('base_url') + turbogears.url('/openid/server')
return dict(person=person, server=server)
diff --git a/fas/fas/static/css/style.css b/fas/fas/static/css/style.css
index e861567..daece0c 100644
--- a/fas/fas/static/css/style.css
+++ b/fas/fas/static/css/style.css
@@ -25,7 +25,7 @@ a
margin-top: 35px;
height: 70px;
line-height: 70px;
- background: url(/fas/static/images/head.png) 0 0 repeat-x;
+ background: url(../images/head.png) 0 0 repeat-x;
}
#head h1
@@ -34,7 +34,7 @@ a
float: left;
text-indent: -9999px;
overflow: hidden;
- background: url(/fas/static/images/logo.png) 1ex 50% no-repeat;
+ background: url(../images/logo.png) 1ex 50% no-repeat;
}
#searchbox
@@ -65,7 +65,7 @@ a
{
height: 30px;
line-height: 30px;
- background: url(/fas/static/images/topnav.png) 0 0 repeat-x;
+ background: url(../images/topnav.png) 0 0 repeat-x;
font-size: 1.6ex;
}
@@ -78,7 +78,7 @@ a
#topnav ul li
{
display: inline;
- background: url(/fas/static/images/topnav-separator.png) 0 50% no-repeat;
+ background: url(../images/topnav-separator.png) 0 50% no-repeat;
padding-left: 3px;
}
@@ -106,7 +106,7 @@ a
right: 0;
height: 35px;
line-height: 35px;
- background: url(/fas/static/images/infobar.png) 0 0 repeat-x;
+ background: url(../images/infobar.png) 0 0 repeat-x;
font-size: 1.6ex;
}
@@ -139,7 +139,7 @@ a
#control ul li
{
display: inline;
- background: url(/fas/static/images/control-separator.png) 0 50% no-repeat;
+ background: url(../images/control-separator.png) 0 50% no-repeat;
}
#control a
@@ -150,14 +150,14 @@ a
#main
{
- background: url(/fas/static/images/shadow.png) 0 0 repeat-x;
+ background: url(../images/shadow.png) 0 0 repeat-x;
}
#sidebar
{
width: 22ex;
float: left;
- background: #335F9D url(/fas/static/images/sidebar.png) 0 0 repeat-x;
+ background: #335F9D url(../images/sidebar.png) 0 0 repeat-x;
border: 1px solid #112233;
}
@@ -246,25 +246,25 @@ a
.account
{
padding-left: 30px;
- background: url(/fas/static/images/account.png) 0 68% no-repeat;
+ background: url(../images/account.png) 0 68% no-repeat;
}
.approved
{
padding-left: 20px;
- background: url(/fas/static/images/approved.png) 0 68% no-repeat;
+ background: url(../images/approved.png) 0 68% no-repeat;
}
.unapproved
{
padding-left: 20px;
- background: url(/fas/static/images/unapproved.png) 0 68% no-repeat;
+ background: url(../images/unapproved.png) 0 68% no-repeat;
}
.attn
{
padding-left: 20px;
- background: url(/fas/static/images/attn.png) 0 68% no-repeat;
+ background: url(../images/attn.png) 0 68% no-repeat;
}
.roleslist
@@ -302,7 +302,7 @@ a
margin-top: 1ex;
padding-top: 1ex;
padding-left: 22px;
- background: url(/fas/static/images/arrow.png) 0 1.6ex no-repeat;
+ background: url(../images/arrow.png) 0 1.6ex no-repeat;
}
#rolespanel h4
@@ -332,13 +332,13 @@ a
#rolespanel .tools li
{
padding-left: 22px;
- background: url(/fas/static/images/tools.png) 0 50% no-repeat;
+ background: url(../images/tools.png) 0 50% no-repeat;
}
#rolespanel .queue li
{
padding-left: 22px;
- background: url(/fas/static/images/queue.png) 0 50% no-repeat;
+ background: url(../images/queue.png) 0 50% no-repeat;
}
#rolespanel .queue strong
@@ -352,7 +352,7 @@ a
clear: both;
text-align: center;
padding: 15px 0 2.5ex;
- background: url(/fas/static/images/footer-top.png) 0 0 repeat-x;
+ background: url(../images/footer-top.png) 0 0 repeat-x;
}
#footer .copy, #footer .disclaimer
@@ -364,7 +364,7 @@ a
{
padding-top: 3px;
padding-bottom: 18px;
- background: #EEEEEE url(/fas/static/images/footer-bottom.png) 0 100% repeat-x;
+ background: #EEEEEE url(../images/footer-bottom.png) 0 100% repeat-x;
list-style: none;
}
@@ -389,7 +389,7 @@ a
.flash
{
- background: #DEE6B1 url(/fas/static/images/success.png) 10px 50% no-repeat;
+ background: #DEE6B1 url(../images/success.png) 10px 50% no-repeat;
border: 1px solid #CCBBAA;
padding: 1.5ex 15px 1.5ex 43px;
margin: 1ex 0;
@@ -397,7 +397,7 @@ a
.help
{
- background: #DEE6B1 url(/fas/static/images/help.png) 10px 50% no-repeat;
+ background: #DEE6B1 url(../images/help.png) 10px 50% no-repeat;
border: 1px solid #CCBBAA;
padding: 1.5ex 15px 1.5ex 65px;
margin: 1ex 0;
diff --git a/fas/fas/static/js/HelpBalloon.js b/fas/fas/static/js/HelpBalloon.js
index 4f19892..cb8e7d8 100644
--- a/fas/fas/static/js/HelpBalloon.js
+++ b/fas/fas/static/js/HelpBalloon.js
@@ -717,7 +717,7 @@ HelpBalloonOptions.prototype = {
* to an existing element if you're using that as your anchoring icon.
* @var {Object}
*/
- icon: '/static/images/balloons/icon.gif',
+ icon: '/accounts/static/images/balloons/icon.gif',
/**
* Alt text of the help icon
* @var {String}
@@ -783,7 +783,7 @@ HelpBalloonOptions.prototype = {
* Clossing button image path
* @var {String}
*/
- button: '/static/images/balloons/button.png',
+ button: '/accounts/static/images/balloons/button.png',
/**
* Balloon image path prefix. There are 4 button images, numerically named, starting with 0.
* 0, 1
@@ -791,7 +791,7 @@ HelpBalloonOptions.prototype = {
* (the number indicates the corner opposite the anchor (the pointing direction)
* @var {String}
*/
- balloonPrefix: '/static/images/balloons/balloon-',
+ balloonPrefix: '/accounts/static/images/balloons/balloon-',
/**
* The image filename suffix, including the file extension
* @var {String}
diff --git a/fas/fas/templates/about.html b/fas/fas/templates/about.html
index c27fe5e..d6dbd9f 100644
--- a/fas/fas/templates/about.html
+++ b/fas/fas/templates/about.html
@@ -8,10 +8,10 @@
${_('FAS - The Open Account System')}
- ${_('FAS is designed around an open architecture. Unlike the traditional account systems where a single admin or group of admins decide who gets to be in what group, FAS is completely designed to be self operating per team. Every group is given at least one administrator who can then approve other people in the group. Also, unlike traditional account systems. FAS allows people to apply for the groups they want to be in. This paridigm is interesting as it allows anyone to find out who is in what groups and contact them. This openness is brought over from the same philosophies that make Open Source popular.')}
+ ${_('''FAS is designed around an open architecture. Unlike the traditional account systems where a single admin or group of admins decide who gets to be in what group, FAS is completely designed to be self operating per team. Every group is given at least one administrator who can then approve other people in the group. Also, unlike traditional account systems. FAS allows people to apply for the groups they want to be in. This paridigm is interesting as it allows anyone to find out who is in what groups and contact them. This openness is brought over from the same philosophies that make Open Source popular.''')}
${_('Etiquette')}
- ${_('People shouldn't assume that by applying for a group that they're then in that group. Consider it like applying for another job. It often takes time. For best odds of success, learn about the group you're applying for and get to know someone in the group. Find someone with sponsor or admin access and ask them if they'd have time to mentor you. Plan on spending at least a few days learning about the group, doing a mundain task, participating on the mailing list. Sometimes this process can take weeks depending on the group. It's best to know you will get sponsored before you apply.')}
+
${_("People shouldn't assume that by applying for a group that they're then in that group. Consider it like applying for another job. It often takes time. For best odds of success, learn about the group you're applying for and get to know someone in the group. Find someone with sponsor or admin access and ask them if they'd have time to mentor you. Plan on spending at least a few days learning about the group, doing a mundain task, participating on the mailing list. Sometimes this process can take weeks depending on the group. It's best to know you will get sponsored before you apply.")}
${_('Users, Sponsors, Administrators')}
- ${_('Once you're in the group, you're in the group. Sponsorship and Administrators typically have special access in the group in questions. Some groups consider sponsorship level to be of a higher involvement, partial ownership of the group for example. But as far as the account system goes the disctinction is easy. Sponsors can approve new users and make people into sponsors. They cannot, however, downgrade or remove other sponsors. They also cannot change administrators in any way. Administrators can do anything to anyone in the group.')}
+
${_('''Once you're in the group, you're in the group. Sponsorship and Administrators typically have special access in the group in questions. Some groups consider sponsorship level to be of a higher involvement, partial ownership of the group for example. But as far as the account system goes the disctinction is easy. Sponsors can approve new users and make people into sponsors. They cannot, however, downgrade or remove other sponsors. They also cannot change administrators in any way. Administrators can do anything to anyone in the group.''')}